andrewhao-gpx 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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