andrewhao-gpx 0.7 → 0.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.travis.yml +8 -0
- data/README.md +98 -0
- data/Rakefile +8 -0
- data/bin/gpx_distance +10 -0
- data/bin/gpx_smooth +63 -0
- data/gpx.gemspec +2 -0
- data/lib/gpx.rb +11 -11
- data/lib/gpx/gpx_file.rb +48 -6
- data/lib/gpx/point.rb +3 -1
- data/lib/gpx/segment.rb +67 -21
- data/lib/gpx/track.rb +7 -0
- data/lib/gpx/trackpoint.rb +2 -0
- data/lib/gpx/version.rb +1 -1
- data/lib/gpx/waypoint.rb +1 -1
- data/tests/gpx10_test.rb +2 -2
- data/tests/gpx_file_test.rb +2 -2
- data/tests/gpx_files/waypoints.gpx +2 -0
- data/tests/magellan_test.rb +2 -2
- data/tests/output_test.rb +4 -3
- data/tests/route_test.rb +2 -2
- data/tests/segment_test.rb +34 -2
- data/tests/track_file_test.rb +2 -2
- data/tests/track_point_test.rb +2 -2
- data/tests/track_test.rb +2 -2
- data/tests/waypoint_test.rb +8 -4
- metadata +45 -25
- data/README.rdoc +0 -46
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5f51764c0a0123d2d6f5a835667a95ec6bee62a6
|
4
|
+
data.tar.gz: 18edbd7be186b8c271ab7a75cb3e685688cfd1a6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d163c7cfcc8cbaad5029280365824fd80d89e9fe9100e871b3a595eb2e35108c487ee744990686f6d951e5d8e1bcc3691dd8defbae1826ba49e1df3b3560b355
|
7
|
+
data.tar.gz: b46cb4436005b206bb4a833ceecc3acf5d200c0169106da710d00cdc03771e3863e7d73e7a664f930d096252d87bc4643ecccfcdf95f197332e798fb863a37bb
|
data/.travis.yml
ADDED
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# GPX Gem
|
2
|
+
|
3
|
+
[<img src="https://travis-ci.org/andrewhao/gpx.svg" alt="Build Status" />](https://travis-ci.org/andrewhao/gpx)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/andrewhao/gpx/badges/gpa.svg)](https://codeclimate.com/github/andrewhao/gpx)
|
5
|
+
|
6
|
+
Copyright (C) 2006 Doug Fales doug@falesafeconsulting.com
|
7
|
+
|
8
|
+
## What It Does
|
9
|
+
|
10
|
+
This library reads GPX files and provides an API for reading and manipulating
|
11
|
+
the data as objects. For more info on the GPX format, see
|
12
|
+
http://www.topografix.com/gpx.asp.
|
13
|
+
|
14
|
+
In addition to parsing GPX files, this library is capable of converting
|
15
|
+
Magellan NMEA files to GPX, and writing new GPX files. It can crop and delete
|
16
|
+
rectangular areas within a file, and it also calculates some meta-data about
|
17
|
+
the tracks and points in a file (such as distance, duration, average speed,
|
18
|
+
etc).
|
19
|
+
|
20
|
+
## Examples
|
21
|
+
|
22
|
+
Reading a GPX file, and cropping its contents to a given area:
|
23
|
+
```ruby
|
24
|
+
gpx = GPX::GPXFile.new(:gpx_file => filename) # Read GPX file
|
25
|
+
bounds = GPX::Bounds.new(params) # Create a rectangular area to crop
|
26
|
+
gpx.crop(bounds) # Crop it
|
27
|
+
gpx.write(filename) # Save it
|
28
|
+
```
|
29
|
+
|
30
|
+
Converting a Magellan track log to GPX:
|
31
|
+
```ruby
|
32
|
+
if GPX::MagellanTrackLog::is_magellan_file?(filename)
|
33
|
+
GPX::MagellanTrackLog::convert_to_gpx(filename, "#{filename}.gpx")
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
Exporting an ActiveRecord to GPXFile (as Waypoints)
|
38
|
+
```ruby
|
39
|
+
#
|
40
|
+
# Our active record in this example is called stop
|
41
|
+
#
|
42
|
+
|
43
|
+
# models/stop.rb
|
44
|
+
class Stop < ActiveRecord::Base
|
45
|
+
# This model has the following attributes:
|
46
|
+
# name
|
47
|
+
# lat
|
48
|
+
# lon
|
49
|
+
# updated_at
|
50
|
+
|
51
|
+
def self.to_gpx
|
52
|
+
require 'GPX'
|
53
|
+
gpx = GPX::GPXFile.new
|
54
|
+
all.each do |stop|
|
55
|
+
gpx.waypoints << GPX::Waypoint.new({name: stop.name, lat: stop.lat, lon: stop.lon, time: stop.updated_at})
|
56
|
+
end
|
57
|
+
gpx.to_s
|
58
|
+
end
|
59
|
+
end # class
|
60
|
+
|
61
|
+
|
62
|
+
# controllers/stops.rb
|
63
|
+
def index
|
64
|
+
@stops = Stop.all
|
65
|
+
respond_to do |format|
|
66
|
+
format.html {render :index}
|
67
|
+
format.gpx { send_data @stops.to_gpx, filename: controller_name + '.gpx' }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# Add this line to config/initializers/mime_types.rb
|
73
|
+
Mime::Type.register "application/gpx+xml", :gpx
|
74
|
+
|
75
|
+
|
76
|
+
# To get the xml file:
|
77
|
+
# http://localhost:3000/stops.gpx
|
78
|
+
```
|
79
|
+
|
80
|
+
You have a complete example on how to create a gpx file from scratch on `tests/output_text.rb`.
|
81
|
+
|
82
|
+
|
83
|
+
## Notes
|
84
|
+
|
85
|
+
This library was written to bridge the gap between my Garmin Geko
|
86
|
+
and my website, WalkingBoss.org (RIP). For that reason, it has always been more of a
|
87
|
+
work-in-progress than an attempt at full GPX compliance. The track side of the
|
88
|
+
library has seen much more use than the route/waypoint side, so if you're doing
|
89
|
+
something with routes or waypoints, you may need to tweak some things.
|
90
|
+
|
91
|
+
Since this code uses XML to read an entire GPX file into memory, it is not
|
92
|
+
the fastest possible solution for working with GPX data, especially if you are
|
93
|
+
working with tracks from several days or weeks.
|
94
|
+
|
95
|
+
Finally, it should be noted that none of the distance/speed calculation or
|
96
|
+
crop/delete code has been tested under International Date Line-crossing
|
97
|
+
conditions. That particular part of the code will likely be unreliable if
|
98
|
+
you're zig-zagging across 180 degrees longitude routinely.
|
data/Rakefile
CHANGED
@@ -5,6 +5,14 @@ require 'rdoc/task'
|
|
5
5
|
desc "Default Task"
|
6
6
|
task :default => [ :test ]
|
7
7
|
|
8
|
+
namespace :ci do
|
9
|
+
task :build do
|
10
|
+
puts "Creating tests/output directory..."
|
11
|
+
FileUtils.mkdir_p "tests/output"
|
12
|
+
Rake::Task[:test].invoke
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
8
16
|
# Run the unit tests
|
9
17
|
desc "Run all unit tests"
|
10
18
|
Rake::TestTask.new("test") { |t|
|
data/bin/gpx_distance
ADDED
data/bin/gpx_smooth
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.expand_path('../../lib/gpx', __FILE__)
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
|
7
|
+
def str_to_int_or_time(str)
|
8
|
+
if str =~ /\A\d{10}\Z/
|
9
|
+
return Time.at(str.to_i)
|
10
|
+
elsif str =~ /\A\d+\Z/
|
11
|
+
return str.to_i
|
12
|
+
else
|
13
|
+
return DateTime.strptime(str, '%Y%m%d-%H:%M:%S').to_time
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
options = {}
|
19
|
+
OptionParser.new do |opts|
|
20
|
+
opts.banner = "Usage: smooth [options]"
|
21
|
+
|
22
|
+
opts.on("-i", "--input-file FILE", "Input file to read gpx data from (if omitted, data will be read from stdin)") do |v|
|
23
|
+
options[:infile] = v
|
24
|
+
end
|
25
|
+
opts.on("-o", "--output-file FILE", "Output file to write smoothed gpx data to (if omitted, data will be written to stdout)") do |v|
|
26
|
+
options[:outfile] = v
|
27
|
+
end
|
28
|
+
opts.on("-s", "--start-time [YYYYMMDD-HH:MM:SS|EPOCH|OFFSET]", "Start smoothing from time or offset specified (if omitted start from the start of the file)") do |v|
|
29
|
+
options[:start] = v
|
30
|
+
end
|
31
|
+
opts.on("-e", "--end-time [YYYYMMDD-HH:MM:SS|EPOCH|OFFSET]", "Finish smoothing from time or offset specified (if omitted finish at the end of the file)") do |v|
|
32
|
+
options[:end] = v
|
33
|
+
end
|
34
|
+
end.parse!
|
35
|
+
|
36
|
+
|
37
|
+
if options[:infile]
|
38
|
+
input = File.open(options[:infile])
|
39
|
+
else
|
40
|
+
input = $stdin
|
41
|
+
end
|
42
|
+
|
43
|
+
options[:start] = str_to_int_or_time(options[:start]) if options[:start]
|
44
|
+
options[:end] = str_to_int_or_time(options[:end]) if options[:end]
|
45
|
+
|
46
|
+
gpx = GPX::GPXFile.new(:gpx_data => input)
|
47
|
+
$stderr.puts "read track with distance #{gpx.distance}"
|
48
|
+
|
49
|
+
#1419062980
|
50
|
+
gpx.tracks.each do |track|
|
51
|
+
track.segments.each do |segment|
|
52
|
+
segment.smooth_location_by_average({:end => options[:end], :start => options[:start]})
|
53
|
+
end
|
54
|
+
end
|
55
|
+
gpx.recalculate_distance
|
56
|
+
$stderr.puts "smoothed distance #{gpx.distance}"
|
57
|
+
|
58
|
+
if options[:outfile]
|
59
|
+
gpx.write(options[:outfile], false)
|
60
|
+
else
|
61
|
+
puts gpx.to_s(false)
|
62
|
+
end
|
63
|
+
|
data/gpx.gemspec
CHANGED
data/lib/gpx.rb
CHANGED
@@ -24,15 +24,15 @@
|
|
24
24
|
require 'time'
|
25
25
|
require 'nokogiri'
|
26
26
|
|
27
|
-
require 'gpx/version'
|
27
|
+
require File.expand_path('../gpx/version', __FILE__)
|
28
28
|
|
29
|
-
require 'gpx/gpx'
|
30
|
-
require 'gpx/gpx_file'
|
31
|
-
require 'gpx/bounds'
|
32
|
-
require 'gpx/track'
|
33
|
-
require 'gpx/route'
|
34
|
-
require 'gpx/segment'
|
35
|
-
require 'gpx/point'
|
36
|
-
require 'gpx/trackpoint'
|
37
|
-
require 'gpx/waypoint'
|
38
|
-
require 'gpx/magellan_track_log'
|
29
|
+
require File.expand_path('../gpx/gpx', __FILE__)
|
30
|
+
require File.expand_path('../gpx/gpx_file', __FILE__)
|
31
|
+
require File.expand_path('../gpx/bounds', __FILE__)
|
32
|
+
require File.expand_path('../gpx/track', __FILE__)
|
33
|
+
require File.expand_path('../gpx/route', __FILE__)
|
34
|
+
require File.expand_path('../gpx/segment', __FILE__)
|
35
|
+
require File.expand_path('../gpx/point', __FILE__)
|
36
|
+
require File.expand_path('../gpx/trackpoint', __FILE__)
|
37
|
+
require File.expand_path('../gpx/waypoint', __FILE__)
|
38
|
+
require File.expand_path('../gpx/magellan_track_log', __FILE__)
|
data/lib/gpx/gpx_file.rb
CHANGED
@@ -45,6 +45,8 @@ module GPX
|
|
45
45
|
#
|
46
46
|
def initialize(opts = {})
|
47
47
|
@duration = 0
|
48
|
+
@attributes = {}
|
49
|
+
@namespace_defs = []
|
48
50
|
if(opts[:gpx_file] or opts[:gpx_data])
|
49
51
|
if opts[:gpx_file]
|
50
52
|
gpx_file = opts[:gpx_file]
|
@@ -54,6 +56,13 @@ module GPX
|
|
54
56
|
@xml = Nokogiri::XML(opts[:gpx_data])
|
55
57
|
end
|
56
58
|
|
59
|
+
gpx_element = @xml.at('gpx')
|
60
|
+
@attributes = gpx_element.attributes
|
61
|
+
@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
|
+
@version = gpx_element['version']
|
57
66
|
reset_meta_data
|
58
67
|
bounds_element = (@xml.at("metadata/bounds") rescue nil)
|
59
68
|
if bounds_element
|
@@ -196,7 +205,7 @@ module GPX
|
|
196
205
|
@time = Time.now if(@time.nil? or update_time)
|
197
206
|
@name ||= File.basename(filename)
|
198
207
|
doc = generate_xml_doc
|
199
|
-
File.open(filename, 'w') { |f| f.write(doc.to_xml) }
|
208
|
+
File.open(filename, 'w+') { |f| f.write(doc.to_xml) }
|
200
209
|
end
|
201
210
|
|
202
211
|
def to_s(update_time = true)
|
@@ -209,17 +218,48 @@ module GPX
|
|
209
218
|
"<#{self.class.name}:...>"
|
210
219
|
end
|
211
220
|
|
221
|
+
def recalculate_distance
|
222
|
+
@distance = 0
|
223
|
+
@tracks.each do |track|
|
224
|
+
track.recalculate_distance
|
225
|
+
@distance += track.distance
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
212
229
|
private
|
230
|
+
def attributes_and_nsdefs_as_gpx_attributes
|
231
|
+
#$stderr.puts @namespace_defs.inspect
|
232
|
+
gpx_header = {}
|
233
|
+
@attributes.each do |k,v|
|
234
|
+
k = v.namespace.prefix + ':' + k if v.namespace
|
235
|
+
gpx_header[k] = v.value
|
236
|
+
end
|
237
|
+
|
238
|
+
@namespace_defs.each do |nsd|
|
239
|
+
tag = 'xmlns'
|
240
|
+
if nsd.prefix
|
241
|
+
tag += ':' + nsd.prefix
|
242
|
+
end
|
243
|
+
gpx_header[tag] = nsd.href
|
244
|
+
end
|
245
|
+
return gpx_header
|
246
|
+
end
|
247
|
+
|
213
248
|
def generate_xml_doc
|
214
249
|
@version ||= '1.1'
|
215
250
|
version_dir = version.gsub('.','/')
|
216
251
|
|
252
|
+
gpx_header = attributes_and_nsdefs_as_gpx_attributes
|
253
|
+
|
254
|
+
gpx_header['version'] = @version.to_s if !gpx_header['version']
|
255
|
+
gpx_header['creator'] = DEFAULT_CREATOR if !gpx_header['creator']
|
256
|
+
gpx_header['xsi:schemaLocation'] = "http://www.topografix.com/GPX/#{version_dir} http://www.topografix.com/GPX/#{version_dir}/gpx.xsd" if !gpx_header['xsi:schemaLocation']
|
257
|
+
gpx_header['xmlns:xsi'] = "http://www.w3.org/2001/XMLSchema-instance" if !gpx_header['xsi'] and !gpx_header['xmlns:xsi']
|
258
|
+
|
259
|
+
#$stderr.puts gpx_header.keys.inspect
|
260
|
+
|
217
261
|
doc = Nokogiri::XML::Builder.new do |xml|
|
218
|
-
xml.gpx(
|
219
|
-
'xsi' => "http://www.w3.org/2001/XMLSchema-instance",
|
220
|
-
'version' => @version.to_s,
|
221
|
-
'creator' => @creator.nil? ? DEFAULT_CREATOR : @creator.to_s,
|
222
|
-
'xsi:schemaLocation' => "http://www.topografix.com/GPX/#{version_dir} http://www.topografix.com/GPX/#{version_dir}/gpx.xsd") \
|
262
|
+
xml.gpx(gpx_header) \
|
223
263
|
{
|
224
264
|
# version 1.0 of the schema doesn't support the metadata element, so push them straight to the root 'gpx' element
|
225
265
|
if (@version == '1.0') then
|
@@ -254,6 +294,7 @@ module GPX
|
|
254
294
|
xml.trkpt(lat: p.lat, lon: p.lon) {
|
255
295
|
xml.time p.time.xmlschema unless p.time.nil?
|
256
296
|
xml.ele p.elevation unless p.elevation.nil?
|
297
|
+
xml << p.extensions.to_xml unless p.extensions.nil?
|
257
298
|
}
|
258
299
|
end
|
259
300
|
}
|
@@ -263,6 +304,7 @@ module GPX
|
|
263
304
|
|
264
305
|
waypoints.each do |w|
|
265
306
|
xml.wpt(lat: w.lat, lon: w.lon) {
|
307
|
+
xml.time w.time.xmlschema unless w.time.nil?
|
266
308
|
Waypoint::SUB_ELEMENTS.each do |sub_elem|
|
267
309
|
xml.send(sub_elem, w.send(sub_elem)) if w.respond_to?(sub_elem) && !w.send(sub_elem).nil?
|
268
310
|
end
|
data/lib/gpx/point.rb
CHANGED
@@ -24,7 +24,7 @@ module GPX
|
|
24
24
|
# The base class for all points. Trackpoint and Waypoint both descend from this base class.
|
25
25
|
class Point < Base
|
26
26
|
D_TO_R = Math::PI/180.0;
|
27
|
-
attr_accessor :lat, :lon, :time, :elevation, :gpx_file, :speed
|
27
|
+
attr_accessor :lat, :lon, :time, :elevation, :gpx_file, :speed, :extensions
|
28
28
|
|
29
29
|
# When you need to manipulate individual points, you can create a Point
|
30
30
|
# object with a latitude, a longitude, an elevation, and a time. In
|
@@ -40,12 +40,14 @@ module GPX
|
|
40
40
|
@time = (Time.xmlschema(elem.at("time").inner_text) rescue nil)
|
41
41
|
@elevation = elem.at("ele").inner_text.to_f unless elem.at("ele").nil?
|
42
42
|
@speed = elem.at("speed").inner_text.to_f unless elem.at("speed").nil?
|
43
|
+
@extensions = elem.at("extensions") unless elem.at("extensions").nil?
|
43
44
|
else
|
44
45
|
@lat = opts[:lat]
|
45
46
|
@lon = opts[:lon]
|
46
47
|
@elevation = opts[:elevation]
|
47
48
|
@time = opts[:time]
|
48
49
|
@speed = opts[:speed]
|
50
|
+
@extensions = opts[:extensions]
|
49
51
|
end
|
50
52
|
|
51
53
|
end
|
data/lib/gpx/segment.rb
CHANGED
@@ -48,23 +48,7 @@ module GPX
|
|
48
48
|
if segment_element.is_a?(Nokogiri::XML::Node)
|
49
49
|
segment_element.search("trkpt").each do |trkpt|
|
50
50
|
pt = TrackPoint.new(:element => trkpt, :segment => self, :gpx_file => @gpx_file)
|
51
|
-
|
52
|
-
@earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
|
53
|
-
@latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
|
54
|
-
end
|
55
|
-
unless pt.elevation.nil?
|
56
|
-
@lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
|
57
|
-
@highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
|
58
|
-
end
|
59
|
-
@bounds.min_lat = pt.lat if pt.lat < @bounds.min_lat
|
60
|
-
@bounds.min_lon = pt.lon if pt.lon < @bounds.min_lon
|
61
|
-
@bounds.max_lat = pt.lat if pt.lat > @bounds.max_lat
|
62
|
-
@bounds.max_lon = pt.lon if pt.lon > @bounds.max_lon
|
63
|
-
|
64
|
-
@distance += haversine_distance(last_pt, pt) unless last_pt.nil?
|
65
|
-
|
66
|
-
@points << pt
|
67
|
-
last_pt = pt
|
51
|
+
append_point(pt)
|
68
52
|
end
|
69
53
|
end
|
70
54
|
end
|
@@ -73,10 +57,14 @@ module GPX
|
|
73
57
|
# Tack on a point to this Segment. All meta-data will be updated.
|
74
58
|
def append_point(pt)
|
75
59
|
last_pt = @points[-1]
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
60
|
+
unless pt.time.nil?
|
61
|
+
@earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
|
62
|
+
@latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
|
63
|
+
end
|
64
|
+
unless pt.elevation.nil?
|
65
|
+
@lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
|
66
|
+
@highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
|
67
|
+
end
|
80
68
|
@bounds.min_lat = pt.lat if pt.lat < @bounds.min_lat
|
81
69
|
@bounds.min_lon = pt.lon if pt.lon < @bounds.min_lon
|
82
70
|
@bounds.max_lat = pt.lat if pt.lat > @bounds.max_lat
|
@@ -144,6 +132,64 @@ module GPX
|
|
144
132
|
result
|
145
133
|
end
|
146
134
|
|
135
|
+
def find_point_by_time_or_offset(indicator)
|
136
|
+
if indicator.nil?
|
137
|
+
return nil
|
138
|
+
elsif indicator.is_a?(Integer)
|
139
|
+
return closest_point(@earliest_point.time + indicator)
|
140
|
+
elsif(indicator.is_a?(Time))
|
141
|
+
return closest_point(indicator)
|
142
|
+
else
|
143
|
+
raise Exception, "find_end_point_by_time_or_offset requires an argument of type Time or Integer"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# smooths the location data in the segment (by recalculating the location as an average of 20 neighbouring points. Useful for removing noise from GPS traces.
|
148
|
+
def smooth_location_by_average(opts={})
|
149
|
+
seconds_either_side = opts[:averaging_window] || 20
|
150
|
+
|
151
|
+
#calculate the first and last points to which the smoothing should be applied
|
152
|
+
earliest = (find_point_by_time_or_offset(opts[:start]) || @earliest_point).time
|
153
|
+
latest = (find_point_by_time_or_offset(opts[:end]) || @latest_point).time
|
154
|
+
|
155
|
+
tmp_points = []
|
156
|
+
|
157
|
+
@points.each do |point|
|
158
|
+
if point.time > latest || point.time < earliest
|
159
|
+
tmp_points.push point #add the point unaltered
|
160
|
+
next
|
161
|
+
end
|
162
|
+
lat_av = 0.to_f
|
163
|
+
lon_av = 0.to_f
|
164
|
+
alt_av = 0.to_f
|
165
|
+
n = 0
|
166
|
+
# k ranges from the time of the current point +/- 20s
|
167
|
+
(-1*seconds_either_side..seconds_either_side).each do |k|
|
168
|
+
# find the point nearest to the time offset indicated by k
|
169
|
+
contributing_point = closest_point(point.time + k)
|
170
|
+
#sum up the contributions to the average
|
171
|
+
lat_av += contributing_point.lat
|
172
|
+
lon_av += contributing_point.lon
|
173
|
+
alt_av += contributing_point.elevation
|
174
|
+
n += 1
|
175
|
+
end
|
176
|
+
# calculate the averages
|
177
|
+
tmp_point = point.clone
|
178
|
+
tmp_point.lon = ((lon_av) / n).round(7)
|
179
|
+
tmp_point.elevation = ((alt_av) / n).round(2)
|
180
|
+
tmp_point.lat = ((lat_av) / n).round(7)
|
181
|
+
tmp_points.push tmp_point
|
182
|
+
end
|
183
|
+
last_pt = nil
|
184
|
+
@distance = 0
|
185
|
+
@points.clear
|
186
|
+
reset_meta_data
|
187
|
+
#now commit the averages back and recalculate the distances
|
188
|
+
tmp_points.each do |point|
|
189
|
+
append_point(point)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
147
193
|
protected
|
148
194
|
def find_closest(pts, time)
|
149
195
|
return pts.first if pts.size == 1
|
data/lib/gpx/track.rb
CHANGED
data/lib/gpx/trackpoint.rb
CHANGED
@@ -30,6 +30,7 @@ module GPX
|
|
30
30
|
|
31
31
|
attr_accessor :segment
|
32
32
|
|
33
|
+
|
33
34
|
def initialize(opts = {})
|
34
35
|
super(opts)
|
35
36
|
@segment = opts[:segment]
|
@@ -54,5 +55,6 @@ module GPX
|
|
54
55
|
def law_of_cosines_distance_from(p2)
|
55
56
|
(Math.acos(Math.sin(latr)*Math.sin(p2.latr) + Math.cos(latr)*Math.cos(p2.latr)*Math.cos(p2.lonr-lonr)) * RADIUS)
|
56
57
|
end
|
58
|
+
|
57
59
|
end
|
58
60
|
end
|
data/lib/gpx/version.rb
CHANGED
data/lib/gpx/waypoint.rb
CHANGED
@@ -25,7 +25,7 @@ module GPX
|
|
25
25
|
# not seen much use yet, since WalkingBoss does not use waypoints right now.
|
26
26
|
class Waypoint < Point
|
27
27
|
|
28
|
-
SUB_ELEMENTS = %w{ magvar geoidheight name cmt desc src link sym type fix sat hdop vdop pdop ageofdgpsdata dgpsid extensions
|
28
|
+
SUB_ELEMENTS = %w{ele magvar geoidheight name cmt desc src link sym type fix sat hdop vdop pdop ageofdgpsdata dgpsid extensions}
|
29
29
|
|
30
30
|
attr_reader :gpx_file
|
31
31
|
SUB_ELEMENTS.each { |sub_el| attr_accessor sub_el.to_sym }
|
data/tests/gpx10_test.rb
CHANGED
data/tests/gpx_file_test.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
require '
|
1
|
+
require 'minitest/autorun'
|
2
2
|
require 'gpx'
|
3
3
|
|
4
|
-
class GPXFileTest < Test
|
4
|
+
class GPXFileTest < Minitest::Test
|
5
5
|
|
6
6
|
ONE_TRACK_FILE = File.join(File.dirname(__FILE__), "gpx_files/one_track.gpx")
|
7
7
|
WITH_OR_WITHOUT_ELEV_FILE = File.join(File.dirname(__FILE__), "gpx_files/with_or_without_elev.gpx")
|
@@ -17,4 +17,6 @@
|
|
17
17
|
<wpt lat="39.999840" lon="-105.214696"><name><![CDATA[SBDR]]></name><sym>Waypoint</sym><ele>1612.965</ele></wpt>
|
18
18
|
<wpt lat="39.989739" lon="-105.295285"><name><![CDATA[TO]]></name><sym>Waypoint</sym><ele>2163.556</ele></wpt>
|
19
19
|
<wpt lat="40.035301" lon="-105.254443"><name><![CDATA[VICS]]></name><sym>Waypoint</sym><ele>1535.34</ele></wpt>
|
20
|
+
<wpt lat="8.992441" lon="-79.530365"><time>2015-01-01T12:12:12Z</time><name>loma la pava ???</name><desc>Mar 22, 2015, 10:46:17</desc>
|
21
|
+
</wpt>
|
20
22
|
</gpx>
|
data/tests/magellan_test.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
require '
|
1
|
+
require 'minitest/autorun'
|
2
2
|
require 'gpx'
|
3
3
|
|
4
|
-
class MagellanTest < Test
|
4
|
+
class MagellanTest < Minitest::Test
|
5
5
|
MAGELLAN_TRACK_LOG = File.join(File.dirname(__FILE__), "gpx_files/magellan_track.log")
|
6
6
|
GPX_FILE = File.join(File.dirname(__FILE__), "gpx_files/one_segment.gpx")
|
7
7
|
|
data/tests/output_test.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
require '
|
1
|
+
require 'minitest/autorun'
|
2
2
|
require 'fileutils'
|
3
3
|
require 'gpx'
|
4
4
|
|
5
|
-
class OutputTest < Test
|
5
|
+
class OutputTest < Minitest::Test
|
6
6
|
|
7
7
|
include GPX
|
8
8
|
|
@@ -42,7 +42,8 @@ class OutputTest < Test::Unit::TestCase
|
|
42
42
|
{:lat => 25.061783, :lon => 121.640267, :name => 'GRMTWN', :sym => 'Waypoint', :ele => '38.09766'},
|
43
43
|
{:lat => 39.999840, :lon => -105.214696, :name => 'SBDR', :sym => 'Waypoint', :ele => '1612.965'},
|
44
44
|
{:lat => 39.989739, :lon => -105.295285, :name => 'TO', :sym => 'Waypoint', :ele => '2163.556'},
|
45
|
-
{:lat => 40.035301, :lon => -105.254443, :name => 'VICS', :sym => 'Waypoint', :ele => '1535.34'}
|
45
|
+
{:lat => 40.035301, :lon => -105.254443, :name => 'VICS', :sym => 'Waypoint', :ele => '1535.34'},
|
46
|
+
{:lat => 40.035301, :lon => -105.254443, :name => 'TIMEDWPT', :sym => 'Waypoint', :ele => '1535.34', :time => Time.parse("2005-12-31T22:05:09Z")}
|
46
47
|
]
|
47
48
|
|
48
49
|
waypoint_data.each do |wpt_hash|
|
data/tests/route_test.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
require '
|
1
|
+
require 'minitest/autorun'
|
2
2
|
require 'gpx'
|
3
3
|
|
4
|
-
class RouteTest < Test
|
4
|
+
class RouteTest < Minitest::Test
|
5
5
|
|
6
6
|
def test_read_routes
|
7
7
|
gpx = GPX::GPXFile.new(:gpx_file => File.join(File.dirname(__FILE__), "gpx_files/routes.gpx"))
|
data/tests/segment_test.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
|
-
require '
|
1
|
+
#require 'minitest/autorun'
|
2
|
+
require 'minitest/autorun'
|
2
3
|
require 'yaml'
|
3
4
|
require 'gpx'
|
4
5
|
|
5
|
-
class SegmentTest < Test
|
6
|
+
class SegmentTest < Minitest::Test
|
6
7
|
ONE_SEGMENT = File.join(File.dirname(__FILE__), "gpx_files/one_segment.gpx")
|
7
8
|
|
8
9
|
def setup
|
@@ -55,4 +56,35 @@ class SegmentTest < Test::Unit::TestCase
|
|
55
56
|
assert_equal(39.188747, @segment.bounds.max_lat)
|
56
57
|
assert_equal(-109.007978, @segment.bounds.max_lon)
|
57
58
|
end
|
59
|
+
|
60
|
+
def test_segment_smooth
|
61
|
+
@segment.smooth_location_by_average
|
62
|
+
assert_equal(189, @segment.points.size)
|
63
|
+
assert_equal(1144433525, @segment.earliest_point.time.to_i)
|
64
|
+
assert_equal(1144437991, @segment.latest_point.time.to_i)
|
65
|
+
assert_equal(1342.58, @segment.lowest_point.elevation)
|
66
|
+
assert_equal(1479.09, @segment.highest_point.elevation)
|
67
|
+
assert_in_delta(6.458085658, @segment.distance, 0.001)
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_segment_smooth_offset
|
71
|
+
@segment.smooth_location_by_average({:start => 1000, :end => 2000})
|
72
|
+
assert_equal(189, @segment.points.size)
|
73
|
+
assert_equal(1144433525, @segment.earliest_point.time.to_i)
|
74
|
+
assert_equal(1144437991, @segment.latest_point.time.to_i)
|
75
|
+
assert_equal(1334.447, @segment.lowest_point.elevation)
|
76
|
+
assert_equal(1480.087, @segment.highest_point.elevation)
|
77
|
+
assert_in_delta(6.900813095, @segment.distance, 0.001)
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_segment_smooth_absolute
|
81
|
+
@segment.smooth_location_by_average({:start => Time.at(1144434520), :end => Time.at(1144435520)})
|
82
|
+
assert_equal(189, @segment.points.size)
|
83
|
+
assert_equal(1144433525, @segment.earliest_point.time.to_i)
|
84
|
+
assert_equal(1144437991, @segment.latest_point.time.to_i)
|
85
|
+
assert_equal(1334.447, @segment.lowest_point.elevation)
|
86
|
+
assert_equal(1480.087, @segment.highest_point.elevation)
|
87
|
+
assert_in_delta(6.900813095, @segment.distance, 0.001)
|
88
|
+
end
|
89
|
+
|
58
90
|
end
|
data/tests/track_file_test.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
require '
|
1
|
+
require 'minitest/autorun'
|
2
2
|
require 'gpx'
|
3
3
|
|
4
|
-
class TrackFileTest < Test
|
4
|
+
class TrackFileTest < Minitest::Test
|
5
5
|
TRACK_FILE = File.join(File.dirname(__FILE__), "gpx_files/tracks.gpx")
|
6
6
|
OTHER_TRACK_FILE = File.join(File.dirname(__FILE__), "gpx_files/arches.gpx")
|
7
7
|
|
data/tests/track_point_test.rb
CHANGED
data/tests/track_test.rb
CHANGED
data/tests/waypoint_test.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
require '
|
1
|
+
require 'minitest/autorun'
|
2
2
|
require 'gpx'
|
3
3
|
|
4
|
-
class WaypointTest < Test
|
4
|
+
class WaypointTest < Minitest::Test
|
5
5
|
|
6
6
|
def test_read_waypoints
|
7
7
|
|
8
8
|
gpx = GPX::GPXFile.new(:gpx_file => File.join(File.dirname(__FILE__), "gpx_files/waypoints.gpx"))
|
9
|
-
assert_equal(
|
9
|
+
assert_equal(18, gpx.waypoints.size)
|
10
10
|
|
11
11
|
# First Waypoint
|
12
12
|
# <wpt lat="40.035557" lon="-105.248268">
|
@@ -38,7 +38,11 @@ class WaypointTest < Test::Unit::TestCase
|
|
38
38
|
assert_equal('002', second_wpt.name)
|
39
39
|
assert_equal('Waypoint', second_wpt.sym)
|
40
40
|
assert_equal(1955.192, second_wpt.elevation)
|
41
|
-
|
41
|
+
|
42
|
+
# test loads time properly in waypoint
|
43
|
+
time_wpt = gpx.waypoints[17]
|
44
|
+
assert_equal(Time.xmlschema("2015-01-01T12:12:12Z"), time_wpt.time)
|
45
|
+
|
42
46
|
end
|
43
47
|
end
|
44
48
|
|
metadata
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: andrewhao-gpx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
5
|
-
prerelease:
|
4
|
+
version: '0.8'
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- Guillaume Dott
|
@@ -11,38 +10,62 @@ authors:
|
|
11
10
|
autorequire:
|
12
11
|
bindir: bin
|
13
12
|
cert_chain: []
|
14
|
-
date:
|
13
|
+
date: 2015-04-20 00:00:00.000000000 Z
|
15
14
|
dependencies:
|
16
15
|
- !ruby/object:Gem::Dependency
|
17
16
|
name: rake
|
18
17
|
requirement: !ruby/object:Gem::Requirement
|
19
|
-
none: false
|
20
18
|
requirements:
|
21
|
-
- -
|
19
|
+
- - ">="
|
22
20
|
- !ruby/object:Gem::Version
|
23
21
|
version: '0'
|
24
22
|
type: :runtime
|
25
23
|
prerelease: false
|
26
24
|
version_requirements: !ruby/object:Gem::Requirement
|
27
|
-
none: false
|
28
25
|
requirements:
|
29
|
-
- -
|
26
|
+
- - ">="
|
30
27
|
- !ruby/object:Gem::Version
|
31
28
|
version: '0'
|
32
29
|
- !ruby/object:Gem::Dependency
|
33
30
|
name: nokogiri
|
34
31
|
requirement: !ruby/object:Gem::Requirement
|
35
|
-
none: false
|
36
32
|
requirements:
|
37
|
-
- -
|
33
|
+
- - ">="
|
38
34
|
- !ruby/object:Gem::Version
|
39
35
|
version: '0'
|
40
36
|
type: :runtime
|
41
37
|
prerelease: false
|
42
38
|
version_requirements: !ruby/object:Gem::Requirement
|
43
|
-
none: false
|
44
39
|
requirements:
|
45
|
-
- -
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: bundler
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
type: :development
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: minitest
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
type: :development
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - ">="
|
46
69
|
- !ruby/object:Gem::Version
|
47
70
|
version: '0'
|
48
71
|
description: A basic API for reading and writing GPX files.
|
@@ -54,11 +77,14 @@ executables: []
|
|
54
77
|
extensions: []
|
55
78
|
extra_rdoc_files: []
|
56
79
|
files:
|
57
|
-
- .gitignore
|
80
|
+
- ".gitignore"
|
81
|
+
- ".travis.yml"
|
58
82
|
- ChangeLog
|
59
83
|
- Gemfile
|
60
|
-
- README.
|
84
|
+
- README.md
|
61
85
|
- Rakefile
|
86
|
+
- bin/gpx_distance
|
87
|
+
- bin/gpx_smooth
|
62
88
|
- gpx.gemspec
|
63
89
|
- lib/gpx.rb
|
64
90
|
- lib/gpx/bounds.rb
|
@@ -95,32 +121,26 @@ files:
|
|
95
121
|
- tests/waypoint_test.rb
|
96
122
|
homepage: http://www.github.com/andrewhao/gpx
|
97
123
|
licenses: []
|
124
|
+
metadata: {}
|
98
125
|
post_install_message:
|
99
126
|
rdoc_options: []
|
100
127
|
require_paths:
|
101
128
|
- lib
|
102
129
|
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
-
none: false
|
104
130
|
requirements:
|
105
|
-
- -
|
131
|
+
- - ">="
|
106
132
|
- !ruby/object:Gem::Version
|
107
133
|
version: '0'
|
108
|
-
segments:
|
109
|
-
- 0
|
110
|
-
hash: -3842906676439924695
|
111
134
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
-
none: false
|
113
135
|
requirements:
|
114
|
-
- -
|
136
|
+
- - ">="
|
115
137
|
- !ruby/object:Gem::Version
|
116
138
|
version: '0'
|
117
|
-
segments:
|
118
|
-
- 0
|
119
|
-
hash: -3842906676439924695
|
120
139
|
requirements: []
|
121
140
|
rubyforge_project:
|
122
|
-
rubygems_version:
|
141
|
+
rubygems_version: 2.4.5
|
123
142
|
signing_key:
|
124
|
-
specification_version:
|
143
|
+
specification_version: 4
|
125
144
|
summary: A basic API for reading and writing GPX files.
|
126
145
|
test_files: []
|
146
|
+
has_rdoc: true
|
data/README.rdoc
DELETED
@@ -1,46 +0,0 @@
|
|
1
|
-
= GPX Gem
|
2
|
-
|
3
|
-
Copyright (C) 2006 Doug Fales mailto:doug@falesafeconsulting.com
|
4
|
-
|
5
|
-
== What It Does
|
6
|
-
|
7
|
-
This library reads GPX files and provides an API for reading and manipulating
|
8
|
-
the data as objects. For more info on the GPX format, see
|
9
|
-
http://www.topografix.com/gpx.asp.
|
10
|
-
|
11
|
-
In addition to parsing GPX files, this library is capable of converting
|
12
|
-
Magellan NMEA files to GPX, and writing new GPX files. It can crop and delete
|
13
|
-
rectangular areas within a file, and it also calculates some meta-data about
|
14
|
-
the tracks and points in a file (such as distance, duration, average speed,
|
15
|
-
etc).
|
16
|
-
|
17
|
-
== Examples
|
18
|
-
|
19
|
-
Reading a GPX file, and cropping its contents to a given area:
|
20
|
-
gpx = GPX::GPXFile.new(:gpx_file => filename) # Read GPX file
|
21
|
-
bounds = GPX::Bounds.new(params) # Create a rectangular area to crop
|
22
|
-
gpx.crop(bounds) # Crop it
|
23
|
-
gpx.write(filename) # Save it
|
24
|
-
|
25
|
-
Converting a Magellan track log to GPX:
|
26
|
-
if GPX::MagellanTrackLog::is_magellan_file?(filename)
|
27
|
-
GPX::MagellanTrackLog::convert_to_gpx(filename, "#{filename}.gpx")
|
28
|
-
end
|
29
|
-
|
30
|
-
|
31
|
-
== Notes
|
32
|
-
|
33
|
-
This library was written to bridge the gap between my Garmin Geko
|
34
|
-
and my website, WalkingBoss.org (RIP). For that reason, it has always been more of a
|
35
|
-
work-in-progress than an attempt at full GPX compliance. The track side of the
|
36
|
-
library has seen much more use than the route/waypoint side, so if you're doing
|
37
|
-
something with routes or waypoints, you may need to tweak some things.
|
38
|
-
|
39
|
-
Since this code uses XML to read an entire GPX file into memory, it is not
|
40
|
-
the fastest possible solution for working with GPX data, especially if you are
|
41
|
-
working with tracks from several days or weeks.
|
42
|
-
|
43
|
-
Finally, it should be noted that none of the distance/speed calculation or
|
44
|
-
crop/delete code has been tested under International Date Line-crossing
|
45
|
-
conditions. That particular part of the code will likely be unreliable if
|
46
|
-
you're zig-zagging across 180 degrees longitude routinely.
|