andrewhao-gpx 0.7

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.
@@ -0,0 +1,132 @@
1
+ #--
2
+ # Copyright (c) 2006 Doug Fales
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
+ #++
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)
109
+
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
124
+ end
125
+ end
126
+ end
127
+ return false
128
+ end
129
+ end
130
+
131
+ end
132
+ end
data/lib/gpx/point.rb ADDED
@@ -0,0 +1,90 @@
1
+ #--
2
+ # Copyright (c) 2006 Doug Fales
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
+ #++
23
+ module GPX
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
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
+ else
44
+ @lat = opts[:lat]
45
+ @lon = opts[:lon]
46
+ @elevation = opts[:elevation]
47
+ @time = opts[:time]
48
+ @speed = opts[:speed]
49
+ end
50
+
51
+ end
52
+
53
+
54
+ # Returns the latitude and longitude (in that order), separated by the
55
+ # given delimeter. This is useful for passing a point into another API
56
+ # (i.e. the Google Maps javascript API).
57
+ def lat_lon(delim = ', ')
58
+ "#{lat}#{delim}#{lon}"
59
+ end
60
+
61
+ # Returns the longitude and latitude (in that order), separated by the
62
+ # given delimeter. This is useful for passing a point into another API
63
+ # (i.e. the Google Maps javascript API).
64
+ def lon_lat(delim = ', ')
65
+ "#{lon}#{delim}#{lat}"
66
+ end
67
+
68
+ # Latitude in radians.
69
+ def latr
70
+ @latr ||= (@lat * D_TO_R)
71
+ end
72
+
73
+ # Longitude in radians.
74
+ def lonr
75
+ @lonr ||= (@lon * D_TO_R)
76
+ end
77
+
78
+ # Set the latitude (in degrees).
79
+ def lat=(latitude)
80
+ @latr = (latitude * D_TO_R)
81
+ @lat = latitude
82
+ end
83
+
84
+ # Set the longitude (in degrees).
85
+ def lon=(longitude)
86
+ @lonr = (longitude * D_TO_R)
87
+ @lon = longitude
88
+ end
89
+ end
90
+ end
data/lib/gpx/route.rb ADDED
@@ -0,0 +1,58 @@
1
+ #--
2
+ # Copyright (c) 2006 Doug Fales
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
+ #++
23
+ module GPX
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])
44
+ end
45
+
46
+ end
47
+
48
+ # Delete points outside of a given area.
49
+ def crop(area)
50
+ points.delete_if{ |pt| not area.contains? pt }
51
+ end
52
+
53
+ # Delete points within the given area.
54
+ def delete_area(area)
55
+ points.delete_if{ |pt| area.contains? pt }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,207 @@
1
+ #--
2
+ # Copyright (c) 2006 Doug Fales
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
+ #++
23
+ module GPX
24
+ # A segment is the basic container in a GPX file. A Segment contains points
25
+ # (in this lib, they're called TrackPoints). A Track contains Segments. An
26
+ # instance of Segment knows its highest point, lowest point, earliest and
27
+ # latest points, distance, and bounds.
28
+ class Segment < Base
29
+
30
+ attr_reader :earliest_point, :latest_point, :bounds, :highest_point, :lowest_point, :distance
31
+ attr_accessor :points, :track
32
+
33
+ # If a XML::Node object is passed-in, this will initialize a new
34
+ # Segment based on its contents. Otherwise, a blank Segment is created.
35
+ def initialize(opts = {})
36
+ @gpx_file = opts[:gpx_file]
37
+ @track = opts[:track]
38
+ @points = []
39
+ @earliest_point = nil
40
+ @latest_point = nil
41
+ @highest_point = nil
42
+ @lowest_point = nil
43
+ @distance = 0.0
44
+ @bounds = Bounds.new
45
+ if(opts[:element])
46
+ segment_element = opts[:element]
47
+ last_pt = nil
48
+ if segment_element.is_a?(Nokogiri::XML::Node)
49
+ segment_element.search("trkpt").each do |trkpt|
50
+ pt = TrackPoint.new(:element => trkpt, :segment => self, :gpx_file => @gpx_file)
51
+ unless pt.time.nil?
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
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ # Tack on a point to this Segment. All meta-data will be updated.
74
+ def append_point(pt)
75
+ last_pt = @points[-1]
76
+ @earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
77
+ @latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
78
+ @lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
79
+ @highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
80
+ @bounds.min_lat = pt.lat if pt.lat < @bounds.min_lat
81
+ @bounds.min_lon = pt.lon if pt.lon < @bounds.min_lon
82
+ @bounds.max_lat = pt.lat if pt.lat > @bounds.max_lat
83
+ @bounds.max_lon = pt.lon if pt.lon > @bounds.max_lon
84
+ @distance += haversine_distance(last_pt, pt) unless last_pt.nil?
85
+ @points << pt
86
+ end
87
+
88
+ # Returns true if the given time is within this Segment.
89
+ def contains_time?(time)
90
+ (time >= @earliest_point.time and time <= @latest_point.time) rescue false
91
+ end
92
+
93
+ # Finds the closest point in time to the passed-in time argument. Useful
94
+ # for matching up time-based objects (photos, video, etc) with a
95
+ # geographic location.
96
+ def closest_point(time)
97
+ find_closest(points, time)
98
+ end
99
+
100
+ # Deletes all points within this Segment that lie outside of the given
101
+ # area (which should be a Bounds object).
102
+ def crop(area)
103
+ delete_if { |pt| not area.contains?(pt) }
104
+ end
105
+
106
+ # Deletes all points in this Segment that lie within the given area.
107
+ def delete_area(area)
108
+ delete_if{ |pt| area.contains?(pt) }
109
+ end
110
+
111
+ # A handy method that deletes points based on a block that is passed in.
112
+ # If the passed-in block returns true when given a point, then that point
113
+ # is deleted. For example:
114
+ # delete_if{ |pt| area.contains?(pt) }
115
+ def delete_if
116
+ reset_meta_data
117
+ keep_points = []
118
+ last_pt = nil
119
+ points.each do |pt|
120
+ unless yield(pt)
121
+ keep_points << pt
122
+ update_meta_data(pt, last_pt)
123
+ last_pt = pt
124
+ end
125
+ end
126
+ @points = keep_points
127
+ end
128
+
129
+ # Returns true if this Segment has no points.
130
+ def empty?
131
+ (points.nil? or (points.size == 0))
132
+ end
133
+
134
+ # Prints out a nice summary of this Segment.
135
+ def to_s
136
+ result = "Track Segment\n"
137
+ result << "\tSize: #{points.size} points\n"
138
+ result << "\tDistance: #{distance} km\n"
139
+ result << "\tEarliest Point: #{earliest_point.time.to_s} \n"
140
+ result << "\tLatest Point: #{latest_point.time.to_s} \n"
141
+ result << "\tLowest Point: #{lowest_point.elevation} \n"
142
+ result << "\tHighest Point: #{highest_point.elevation}\n "
143
+ result << "\tBounds: #{bounds.to_s}"
144
+ result
145
+ end
146
+
147
+ protected
148
+ def find_closest(pts, time)
149
+ return pts.first if pts.size == 1
150
+ midpoint = pts.size/2
151
+ if pts.size == 2
152
+ diff_1 = pts[0].time - time
153
+ diff_2 = pts[1].time - time
154
+ return (diff_1 < diff_2 ? pts[0] : pts[1])
155
+ end
156
+ if time >= pts[midpoint].time and time <= pts[midpoint+1].time
157
+
158
+ return pts[midpoint]
159
+
160
+ elsif(time <= pts[midpoint].time)
161
+ return find_closest(pts[0..midpoint], time)
162
+ else
163
+ return find_closest(pts[(midpoint+1)..-1], time)
164
+ end
165
+ end
166
+
167
+ # Calculate the Haversine distance between two points. This is the method
168
+ # the library uses to calculate the cumulative distance of GPX files.
169
+ def haversine_distance(p1, p2)
170
+ p1.haversine_distance_from(p2)
171
+ end
172
+
173
+ # Calculate the plain Pythagorean difference between two points. Not currently used.
174
+ def pythagorean_distance(p1, p2)
175
+ p1.pythagorean_distance_from(p2)
176
+ end
177
+
178
+ # Calculates the distance between two points using the Law of Cosines formula. Not currently used.
179
+ def law_of_cosines_distance(p1, p2)
180
+ p1.law_of_cosines_distance_from(p2)
181
+ end
182
+
183
+ def reset_meta_data
184
+ @earliest_point = nil
185
+ @latest_point = nil
186
+ @highest_point = nil
187
+ @lowest_point = nil
188
+ @distance = 0.0
189
+ @bounds = Bounds.new
190
+ end
191
+
192
+ def update_meta_data(pt, last_pt)
193
+ unless pt.time.nil?
194
+ @earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
195
+ @latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
196
+ end
197
+ unless pt.elevation.nil?
198
+ @lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
199
+ @highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
200
+ end
201
+ @bounds.add(pt)
202
+ @distance += haversine_distance(last_pt, pt) unless last_pt.nil?
203
+ end
204
+
205
+ end
206
+
207
+ end