gpx 0.7 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.md +82 -0
- data/Gemfile +4 -0
- data/README.md +100 -0
- data/Rakefile +10 -31
- data/bin/gpx_distance +10 -0
- data/bin/gpx_smooth +63 -0
- data/gpx.gemspec +17 -9
- data/lib/gpx.rb +16 -16
- data/lib/gpx/bounds.rb +43 -52
- data/lib/gpx/gpx.rb +17 -20
- data/lib/gpx/gpx_file.rb +292 -217
- data/lib/gpx/magellan_track_log.rb +105 -107
- data/lib/gpx/point.rb +58 -74
- data/lib/gpx/route.rb +31 -43
- data/lib/gpx/segment.rb +229 -198
- data/lib/gpx/track.rb +103 -109
- data/lib/gpx/trackpoint.rb +37 -12
- data/lib/gpx/version.rb +3 -0
- data/lib/gpx/waypoint.rb +3 -25
- data/tests/gpx10_test.rb +3 -3
- data/tests/gpx_file_test.rb +27 -25
- data/tests/gpx_files/arches.gpx +1 -1
- data/tests/gpx_files/big.gpx +1 -1
- data/tests/gpx_files/one_track.gpx +2 -1
- data/tests/gpx_files/waypoints.gpx +2 -0
- data/tests/magellan_test.rb +3 -3
- data/tests/output_test.rb +14 -17
- data/tests/route_test.rb +7 -7
- data/tests/segment_test.rb +40 -8
- data/tests/track_file_test.rb +3 -3
- data/tests/track_point_test.rb +30 -0
- data/tests/track_test.rb +20 -20
- data/tests/waypoint_test.rb +11 -7
- metadata +61 -10
- data/ChangeLog +0 -60
- data/Gemfile.lock +0 -23
- data/README.rdoc +0 -46
- data/tests/output/myoutput.gpx +0 -759
- data/tests/output/new_gpx_file_from_scratch.gpx +0 -105
@@ -1,5 +1,5 @@
|
|
1
1
|
#--
|
2
|
-
# Copyright (c) 2006 Doug Fales
|
2
|
+
# Copyright (c) 2006 Doug Fales
|
3
3
|
#
|
4
4
|
# Permission is hereby granted, free of charge, to any person obtaining
|
5
5
|
# a copy of this software and associated documentation files (the
|
@@ -21,114 +21,112 @@
|
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
22
|
#++
|
23
23
|
module GPX
|
24
|
+
# This class will parse the lat/lon and time data from a Magellan track log,
|
25
|
+
# which is a NMEA formatted CSV list of points.
|
26
|
+
class MagellanTrackLog
|
27
|
+
#PMGNTRK
|
28
|
+
# This message is to be used to transmit Track information (basically a list of previous position fixes)
|
29
|
+
# which is often displayed on the plotter or map screen of the unit. The first field in this message
|
30
|
+
# is the Latitude, followed by N or S. The next field is the Longitude followed by E or W. The next
|
31
|
+
# field is the altitude followed by “F” for feet or “M” for meters. The next field is
|
32
|
+
# the UTC time of the fix. The next field consists of a status letter of “A” to indicated that
|
33
|
+
# the data is valid, or “V” to indicate that the data is not valid. The last character field is
|
34
|
+
# the name of the track, for those units that support named tracks. The last field contains the UTC date
|
35
|
+
# of the fix. Note that this field is (and its preceding comma) is only produced by the unit when the
|
36
|
+
# command PMGNCMD,TRACK,2 is given. It is not present when a simple command of PMGNCMD,TRACK is issued.
|
37
|
+
|
38
|
+
#NOTE: The Latitude and Longitude Fields are shown as having two decimal
|
39
|
+
# places. As many additional decimal places may be added as long as the total
|
40
|
+
# length of the message does not exceed 82 bytes.
|
41
|
+
|
42
|
+
# $PMGNTRK,llll.ll,a,yyyyy.yy,a,xxxxx,a,hhmmss.ss,A,c----c,ddmmyy*hh<CR><LF>
|
43
|
+
require 'csv'
|
44
|
+
|
45
|
+
LAT = 1
|
46
|
+
LAT_HEMI = 2
|
47
|
+
LON = 3
|
48
|
+
LON_HEMI = 4
|
49
|
+
ELE = 5
|
50
|
+
ELE_UNITS = 6
|
51
|
+
TIME = 7
|
52
|
+
INVALID_FLAG = 8
|
53
|
+
DATE = 10
|
54
|
+
|
55
|
+
FEET_TO_METERS = 0.3048
|
56
|
+
|
57
|
+
class << self
|
58
|
+
|
59
|
+
# Takes the name of a magellan file, converts the contents to GPX, and
|
60
|
+
# writes the result to gpx_filename.
|
61
|
+
def convert_to_gpx(magellan_filename, gpx_filename)
|
62
|
+
|
63
|
+
segment = Segment.new
|
64
|
+
|
65
|
+
CSV.open(magellan_filename, "r").each do |row|
|
66
|
+
next if(row.size < 10 or row[INVALID_FLAG] == 'V')
|
67
|
+
|
68
|
+
lat_deg = row[LAT][0..1]
|
69
|
+
lat_min = row[LAT][2...-1]
|
70
|
+
lat_hemi = row[LAT_HEMI]
|
71
|
+
|
72
|
+
lat = lat_deg.to_f + (lat_min.to_f / 60.0)
|
73
|
+
lat = (-lat) if(lat_hemi == 'S')
|
74
|
+
|
75
|
+
lon_deg = row[LON][0..2]
|
76
|
+
lon_min = row[LON][3..-1]
|
77
|
+
lon_hemi = row[LON_HEMI]
|
78
|
+
|
79
|
+
lon = lon_deg.to_f + (lon_min.to_f / 60.0)
|
80
|
+
lon = (-lon) if(lon_hemi == 'W')
|
81
|
+
|
82
|
+
|
83
|
+
ele = row[ELE]
|
84
|
+
ele_units = row[ELE_UNITS]
|
85
|
+
ele = ele.to_f
|
86
|
+
if(ele_units == 'F')
|
87
|
+
ele *= FEET_TO_METERS
|
88
|
+
end
|
89
|
+
|
90
|
+
hrs = row[TIME][0..1].to_i
|
91
|
+
mins = row[TIME][2..3].to_i
|
92
|
+
secs = row[TIME][4..5].to_i
|
93
|
+
day = row[DATE][0..1].to_i
|
94
|
+
mon = row[DATE][2..3].to_i
|
95
|
+
yr = 2000 + row[DATE][4..5].to_i
|
96
|
+
|
97
|
+
time = Time.gm(yr, mon, day, hrs, mins, secs)
|
98
|
+
|
99
|
+
#must create point
|
100
|
+
pt = TrackPoint.new(:lat => lat, :lon => lon, :time => time, :elevation => ele)
|
101
|
+
segment.append_point(pt)
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
trk = Track.new
|
106
|
+
trk.append_segment(segment)
|
107
|
+
gpx_file = GPXFile.new(:tracks => [trk])
|
108
|
+
gpx_file.write(gpx_filename)
|
24
109
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
#NOTE: The Latitude and Longitude Fields are shown as having two decimal
|
41
|
-
# places. As many additional decimal places may be added as long as the total
|
42
|
-
# length of the message does not exceed 82 bytes.
|
43
|
-
|
44
|
-
# $PMGNTRK,llll.ll,a,yyyyy.yy,a,xxxxx,a,hhmmss.ss,A,c----c,ddmmyy*hh<CR><LF>
|
45
|
-
require 'csv'
|
46
|
-
|
47
|
-
LAT = 1
|
48
|
-
LAT_HEMI = 2
|
49
|
-
LON = 3
|
50
|
-
LON_HEMI = 4
|
51
|
-
ELE = 5
|
52
|
-
ELE_UNITS = 6
|
53
|
-
TIME = 7
|
54
|
-
INVALID_FLAG = 8
|
55
|
-
DATE = 10
|
56
|
-
|
57
|
-
FEET_TO_METERS = 0.3048
|
58
|
-
|
59
|
-
class << self
|
60
|
-
|
61
|
-
# Takes the name of a magellan file, converts the contents to GPX, and
|
62
|
-
# writes the result to gpx_filename.
|
63
|
-
def convert_to_gpx(magellan_filename, gpx_filename)
|
64
|
-
|
65
|
-
segment = Segment.new
|
66
|
-
|
67
|
-
CSV.open(magellan_filename, "r").each do |row|
|
68
|
-
next if(row.size < 10 or row[INVALID_FLAG] == 'V')
|
69
|
-
|
70
|
-
lat_deg = row[LAT][0..1]
|
71
|
-
lat_min = row[LAT][2...-1]
|
72
|
-
lat_hemi = row[LAT_HEMI]
|
73
|
-
|
74
|
-
lat = lat_deg.to_f + (lat_min.to_f / 60.0)
|
75
|
-
lat = (-lat) if(lat_hemi == 'S')
|
76
|
-
|
77
|
-
lon_deg = row[LON][0..2]
|
78
|
-
lon_min = row[LON][3..-1]
|
79
|
-
lon_hemi = row[LON_HEMI]
|
80
|
-
|
81
|
-
lon = lon_deg.to_f + (lon_min.to_f / 60.0)
|
82
|
-
lon = (-lon) if(lon_hemi == 'W')
|
83
|
-
|
84
|
-
|
85
|
-
ele = row[ELE]
|
86
|
-
ele_units = row[ELE_UNITS]
|
87
|
-
ele = ele.to_f
|
88
|
-
if(ele_units == 'F')
|
89
|
-
ele *= FEET_TO_METERS
|
90
|
-
end
|
91
|
-
|
92
|
-
hrs = row[TIME][0..1].to_i
|
93
|
-
mins = row[TIME][2..3].to_i
|
94
|
-
secs = row[TIME][4..5].to_i
|
95
|
-
day = row[DATE][0..1].to_i
|
96
|
-
mon = row[DATE][2..3].to_i
|
97
|
-
yr = 2000 + row[DATE][4..5].to_i
|
98
|
-
|
99
|
-
time = Time.gm(yr, mon, day, hrs, mins, secs)
|
100
|
-
|
101
|
-
#must create point
|
102
|
-
pt = TrackPoint.new(:lat => lat, :lon => lon, :time => time, :elevation => ele)
|
103
|
-
segment.append_point(pt)
|
104
|
-
|
105
|
-
end
|
106
|
-
|
107
|
-
trk = Track.new
|
108
|
-
trk.append_segment(segment)
|
109
|
-
gpx_file = GPXFile.new(:tracks => [trk])
|
110
|
-
gpx_file.write(gpx_filename)
|
111
|
-
|
112
|
-
end
|
113
|
-
|
114
|
-
# Tests to see if the given file is a magellan NMEA track log.
|
115
|
-
def is_magellan_file?(filename)
|
116
|
-
i = 0
|
117
|
-
File.open(filename, "r") do |f|
|
118
|
-
f.each do |line|
|
119
|
-
i += 1
|
120
|
-
if line =~ /^\$PMGNTRK/
|
121
|
-
return true
|
122
|
-
elsif line =~ /<\?xml/
|
123
|
-
return false
|
124
|
-
elsif(i > 10)
|
125
|
-
return false
|
126
|
-
end
|
127
|
-
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Tests to see if the given file is a magellan NMEA track log.
|
113
|
+
def is_magellan_file?(filename)
|
114
|
+
i = 0
|
115
|
+
File.open(filename, "r") do |f|
|
116
|
+
f.each do |line|
|
117
|
+
i += 1
|
118
|
+
if line =~ /^\$PMGNTRK/
|
119
|
+
return true
|
120
|
+
elsif line =~ /<\?xml/
|
121
|
+
return false
|
122
|
+
elsif(i > 10)
|
123
|
+
return false
|
128
124
|
end
|
129
|
-
|
130
|
-
|
125
|
+
end
|
126
|
+
end
|
127
|
+
return false
|
131
128
|
end
|
129
|
+
end
|
132
130
|
|
133
|
-
|
131
|
+
end
|
134
132
|
end
|
data/lib/gpx/point.rb
CHANGED
@@ -20,89 +20,73 @@
|
|
20
20
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
22
|
#++
|
23
|
-
include Math
|
24
23
|
module GPX
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
# When you need to manipulate individual points, you can create a Point
|
31
|
-
# object with a latitude, a longitude, an elevation, and a time. In
|
32
|
-
# addition, you can pass an XML element to this initializer, and the
|
33
|
-
# relevant info will be parsed out.
|
34
|
-
def initialize(opts = {:lat => 0.0, :lon => 0.0, :elevation => 0.0, :time => Time.now } )
|
35
|
-
@gpx_file = opts[:gpx_file]
|
36
|
-
if (opts[:element])
|
37
|
-
elem = opts[:element]
|
38
|
-
@lat, @lon = elem["lat"].to_f, elem["lon"].to_f
|
39
|
-
@latr, @lonr = (D_TO_R * @lat), (D_TO_R * @lon)
|
40
|
-
#'-'? yyyy '-' mm '-' dd 'T' hh ':' mm ':' ss ('.' s+)? (zzzzzz)?
|
41
|
-
@time = (Time.xmlschema(elem.at("time").inner_text) rescue nil)
|
42
|
-
@elevation = elem.at("ele").inner_text.to_f unless elem.at("ele").nil?
|
43
|
-
@speed = elem.at("speed").inner_text.to_f unless elem.at("speed").nil?
|
44
|
-
else
|
45
|
-
@lat = opts[:lat]
|
46
|
-
@lon = opts[:lon]
|
47
|
-
@elevation = opts[:elevation]
|
48
|
-
@time = opts[:time]
|
49
|
-
@speed = opts[:speed]
|
50
|
-
end
|
24
|
+
# The base class for all points. Trackpoint and Waypoint both descend from this base class.
|
25
|
+
class Point < Base
|
26
|
+
D_TO_R = Math::PI/180.0;
|
27
|
+
attr_accessor :lat, :lon, :time, :elevation, :gpx_file, :speed, :extensions
|
51
28
|
|
29
|
+
# When you need to manipulate individual points, you can create a Point
|
30
|
+
# object with a latitude, a longitude, an elevation, and a time. In
|
31
|
+
# addition, you can pass an XML element to this initializer, and the
|
32
|
+
# relevant info will be parsed out.
|
33
|
+
def initialize(opts = {:lat => 0.0, :lon => 0.0, :elevation => 0.0, :time => Time.now } )
|
34
|
+
@gpx_file = opts[:gpx_file]
|
35
|
+
if (opts[:element])
|
36
|
+
elem = opts[:element]
|
37
|
+
@lat, @lon = elem["lat"].to_f, elem["lon"].to_f
|
38
|
+
@latr, @lonr = (D_TO_R * @lat), (D_TO_R * @lon)
|
39
|
+
#'-'? yyyy '-' mm '-' dd 'T' hh ':' mm ':' ss ('.' s+)? (zzzzzz)?
|
40
|
+
@time = (Time.xmlschema(elem.at("time").inner_text) rescue nil)
|
41
|
+
@elevation = elem.at("ele").inner_text.to_f unless elem.at("ele").nil?
|
42
|
+
@speed = elem.at("speed").inner_text.to_f unless elem.at("speed").nil?
|
43
|
+
@extensions = elem.at("extensions") unless elem.at("extensions").nil?
|
44
|
+
else
|
45
|
+
@lat = opts[:lat]
|
46
|
+
@lon = opts[:lon]
|
47
|
+
@elevation = opts[:elevation]
|
48
|
+
@time = opts[:time]
|
49
|
+
@speed = opts[:speed]
|
50
|
+
@extensions = opts[:extensions]
|
52
51
|
end
|
53
52
|
|
53
|
+
end
|
54
54
|
|
55
|
-
# Returns the latitude and longitude (in that order), separated by the
|
56
|
-
# given delimeter. This is useful for passing a point into another API
|
57
|
-
# (i.e. the Google Maps javascript API).
|
58
|
-
def lat_lon(delim = ', ')
|
59
|
-
"#{lat}#{delim}#{lon}"
|
60
|
-
end
|
61
55
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
56
|
+
# Returns the latitude and longitude (in that order), separated by the
|
57
|
+
# given delimeter. This is useful for passing a point into another API
|
58
|
+
# (i.e. the Google Maps javascript API).
|
59
|
+
def lat_lon(delim = ', ')
|
60
|
+
"#{lat}#{delim}#{lon}"
|
61
|
+
end
|
68
62
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
63
|
+
# Returns the longitude and latitude (in that order), separated by the
|
64
|
+
# given delimeter. This is useful for passing a point into another API
|
65
|
+
# (i.e. the Google Maps javascript API).
|
66
|
+
def lon_lat(delim = ', ')
|
67
|
+
"#{lon}#{delim}#{lat}"
|
68
|
+
end
|
73
69
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
70
|
+
# Latitude in radians.
|
71
|
+
def latr
|
72
|
+
@latr ||= (@lat * D_TO_R)
|
73
|
+
end
|
78
74
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
end
|
84
|
-
|
85
|
-
# Set the longitude (in degrees).
|
86
|
-
def lon=(longitude)
|
87
|
-
@lonr = (longitude * D_TO_R)
|
88
|
-
@lon = longitude
|
89
|
-
end
|
75
|
+
# Longitude in radians.
|
76
|
+
def lonr
|
77
|
+
@lonr ||= (@lon * D_TO_R)
|
78
|
+
end
|
90
79
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
unless time.nil?
|
97
|
-
time_elem = Node.new('time')
|
98
|
-
time_elem << time.xmlschema
|
99
|
-
pt << time_elem
|
100
|
-
end
|
101
|
-
elev = Node.new('ele')
|
102
|
-
elev << elevation
|
103
|
-
pt << elev
|
104
|
-
pt
|
105
|
-
end
|
80
|
+
# Set the latitude (in degrees).
|
81
|
+
def lat=(latitude)
|
82
|
+
@latr = (latitude * D_TO_R)
|
83
|
+
@lat = latitude
|
84
|
+
end
|
106
85
|
|
107
|
-
|
86
|
+
# Set the longitude (in degrees).
|
87
|
+
def lon=(longitude)
|
88
|
+
@lonr = (longitude * D_TO_R)
|
89
|
+
@lon = longitude
|
90
|
+
end
|
91
|
+
end
|
108
92
|
end
|
data/lib/gpx/route.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#--
|
2
|
-
# Copyright (c) 2006 Doug Fales
|
2
|
+
# Copyright (c) 2006 Doug Fales
|
3
3
|
#
|
4
4
|
# Permission is hereby granted, free of charge, to any person obtaining
|
5
5
|
# a copy of this software and associated documentation files (the
|
@@ -21,50 +21,38 @@
|
|
21
21
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
22
22
|
#++
|
23
23
|
module GPX
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@name = (opts[:name])
|
45
|
-
end
|
46
|
-
|
24
|
+
# A Route in GPX is very similar to a Track, but it is created by a user
|
25
|
+
# from a series of Waypoints, whereas a Track is created by the GPS device
|
26
|
+
# automatically logging your progress at regular intervals.
|
27
|
+
class Route < Base
|
28
|
+
|
29
|
+
attr_accessor :points, :name, :gpx_file
|
30
|
+
|
31
|
+
# Initialize a Route from a XML::Node.
|
32
|
+
def initialize(opts = {})
|
33
|
+
if(opts[:gpx_file] and opts[:element])
|
34
|
+
rte_element = opts[:element]
|
35
|
+
@gpx_file = opts[:gpx_file]
|
36
|
+
@name = rte_element.at("name").inner_text
|
37
|
+
@points = []
|
38
|
+
rte_element.search("rtept").each do |point|
|
39
|
+
@points << Point.new(:element => point, :gpx_file => @gpx_file)
|
40
|
+
end
|
41
|
+
else
|
42
|
+
@points = (opts[:points] or [])
|
43
|
+
@name = (opts[:name])
|
47
44
|
end
|
48
45
|
|
49
|
-
|
50
|
-
def crop(area)
|
51
|
-
points.delete_if{ |pt| not area.contains? pt }
|
52
|
-
end
|
53
|
-
|
54
|
-
# Delete points within the given area.
|
55
|
-
def delete_area(area)
|
56
|
-
points.delete_if{ |pt| area.contains? pt }
|
57
|
-
end
|
46
|
+
end
|
58
47
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
name_elem << name
|
64
|
-
rte << name_elem
|
65
|
-
points.each { |rte_pt| rte << rte_pt.to_xml('rtept') }
|
66
|
-
rte
|
67
|
-
end
|
48
|
+
# Delete points outside of a given area.
|
49
|
+
def crop(area)
|
50
|
+
points.delete_if{ |pt| not area.contains? pt }
|
51
|
+
end
|
68
52
|
|
69
|
-
|
53
|
+
# Delete points within the given area.
|
54
|
+
def delete_area(area)
|
55
|
+
points.delete_if{ |pt| area.contains? pt }
|
56
|
+
end
|
57
|
+
end
|
70
58
|
end
|