gpx 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,134 @@
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
+
25
+ # This class will parse the lat/lon and time data from a Magellan track log,
26
+ # which is a NMEA formatted CSV list of points.
27
+
28
+ class MagellanTrackLog
29
+ #PMGNTRK
30
+ # This message is to be used to transmit Track information (basically a list of previous position fixes)
31
+ # which is often displayed on the plotter or map screen of the unit. The first field in this message
32
+ # is the Latitude, followed by N or S. The next field is the Longitude followed by E or W. The next
33
+ # field is the altitude followed by “F” for feet or “M” for meters. The next field is
34
+ # the UTC time of the fix. The next field consists of a status letter of “A” to indicated that
35
+ # the data is valid, or “V” to indicate that the data is not valid. The last character field is
36
+ # the name of the track, for those units that support named tracks. The last field contains the UTC date
37
+ # of the fix. Note that this field is (and its preceding comma) is only produced by the unit when the
38
+ # command PMGNCMD,TRACK,2 is given. It is not present when a simple command of PMGNCMD,TRACK is issued.
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") 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
128
+ end
129
+ return false
130
+ end
131
+ end
132
+
133
+ end
134
+ end
data/lib/gpx/point.rb ADDED
@@ -0,0 +1,103 @@
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
+ include Math
24
+ module GPX
25
+ # The base class for all points. Trackpoint and Waypoint both descend from this base class.
26
+ class Point < Base
27
+ D_TO_R = PI/180.0;
28
+ attr_accessor :lat, :lon, :time, :elevation
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 a REXML 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
+ if (opts[:element])
36
+ elem = opts[:element]
37
+ @lat, @lon = elem.attributes["lat"].to_f, elem.attributes["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.elements["time"].text) rescue nil)
41
+ @elevation = elem.elements["ele"].text.to_f if elem.elements["ele"]
42
+ else
43
+ @lat = opts[:lat]
44
+ @lon = opts[:lon]
45
+ @elevation = opts[:elevation]
46
+ @time = opts[:time]
47
+ end
48
+
49
+ end
50
+
51
+
52
+ # Returns the latitude and longitude (in that order), separated by the
53
+ # given delimeter. This is useful for passing a point into another API
54
+ # (i.e. the Google Maps javascript API).
55
+ def lat_lon(delim = ', ')
56
+ "#{lat}#{delim}#{lon}"
57
+ end
58
+
59
+ # Returns the longitude and latitude (in that order), separated by the
60
+ # given delimeter. This is useful for passing a point into another API
61
+ # (i.e. the Google Maps javascript API).
62
+ def lon_lat(delim = ', ')
63
+ "#{lon}#{delim}#{lat}"
64
+ end
65
+
66
+ # Latitude in radians.
67
+ def latr
68
+ @latr ||= (@lat * D_TO_R)
69
+ end
70
+
71
+ # Longitude in radians.
72
+ def lonr
73
+ @lonr ||= (@lon * D_TO_R)
74
+ end
75
+
76
+ # Set the latitude (in degrees).
77
+ def lat=(latitude)
78
+ @latr = (latitude * D_TO_R)
79
+ @lat = latitude
80
+ end
81
+
82
+ # Set the longitude (in degrees).
83
+ def lon=(longitude)
84
+ @lonr = (longitude * D_TO_R)
85
+ @lon = longitude
86
+ end
87
+
88
+ # Convert this point to a REXML::Element.
89
+ def to_xml(elem_name = 'trkpt')
90
+ pt = Element.new('trkpt')
91
+ pt.attributes['lat'] = lat
92
+ pt.attributes['lon'] = lon
93
+ time_elem = Element.new('time')
94
+ time_elem.text = time.xmlschema
95
+ pt.elements << time_elem
96
+ elev = Element.new('ele')
97
+ elev.text = elevation
98
+ pt.elements << elev
99
+ pt
100
+ end
101
+
102
+ end
103
+ end
data/lib/gpx/route.rb ADDED
@@ -0,0 +1,65 @@
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
+
25
+ # A Route in GPX is very similar to a Track, but it is created by a user
26
+ # from a series of Waypoints, whereas a Track is created by the GPS device
27
+ # automatically logging your progress at regular intervals.
28
+ class Route < Base
29
+
30
+ attr_reader :points, :name, :gpx_file
31
+
32
+ # Initialize a Route from a REXML::Element.
33
+ def initialize(opts = {})
34
+ rte_element = opts[:element]
35
+ @gpx_file = opts[:gpx_file]
36
+ @name = rte_element.elements["child::name"].text
37
+ @points = []
38
+ XPath.each(rte_element, "child::rtept") do |point|
39
+ @points << Point.new(:element => point)
40
+ end
41
+
42
+ end
43
+
44
+ # Delete points outside of a given area.
45
+ def crop(area)
46
+ points.delete_if{ |pt| not area.contains? pt }
47
+ end
48
+
49
+ # Delete points within the given area.
50
+ def delete_area(area)
51
+ points.delete_if{ |pt| area.contains? pt }
52
+ end
53
+
54
+ # Convert this Route to a REXML::Element.
55
+ def to_xml
56
+ rte = Element.new('rte')
57
+ name_elem = Element.new('name')
58
+ name_elem.text = name
59
+ rte.elements << name_elem
60
+ points.each { |rte_pt| rte.elements << rte_pt.to_xml('rtept') }
61
+ rte
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,217 @@
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
+
25
+ # A segment is the basic container in a GPX file. A Segment contains points
26
+ # (in this lib, they're called TrackPoints). A Track contains Segments. An
27
+ # instance of Segment knows its highest point, lowest point, earliest and
28
+ # latest points, distance, and bounds.
29
+ class Segment < Base
30
+
31
+ attr_reader :earliest_point, :latest_point, :bounds, :highest_point, :lowest_point, :distance
32
+ attr_accessor :points, :track
33
+
34
+ # If a REXML::Element object is passed-in, this will initialize a new
35
+ # Segment based on its contents. Otherwise, a blank Segment is created.
36
+ def initialize(opts = {})
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
+ unless segment_element.is_a?(Text)
49
+ XPath.each(segment_element, "child::trkpt") do |trkpt|
50
+ pt = TrackPoint.new(:element => trkpt, :segment => self)
51
+ @earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
52
+ @latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
53
+ unless pt.elevation.nil?
54
+ @lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
55
+ @highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
56
+ end
57
+ @bounds.min_lat = pt.lat if pt.lat < @bounds.min_lat
58
+ @bounds.min_lon = pt.lon if pt.lon < @bounds.min_lon
59
+ @bounds.max_lat = pt.lat if pt.lat > @bounds.max_lat
60
+ @bounds.max_lon = pt.lon if pt.lon > @bounds.max_lon
61
+
62
+ @distance += haversine_distance(last_pt, pt) unless last_pt.nil?
63
+
64
+ @points << pt
65
+ last_pt = pt
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ # Tack on a point to this Segment. All meta-data will be updated.
72
+ def append_point(pt)
73
+ last_pt = @points[-1]
74
+ @earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
75
+ @latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
76
+ @lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
77
+ @highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
78
+ @bounds.min_lat = pt.lat if pt.lat < @bounds.min_lat
79
+ @bounds.min_lon = pt.lon if pt.lon < @bounds.min_lon
80
+ @bounds.max_lat = pt.lat if pt.lat > @bounds.max_lat
81
+ @bounds.max_lon = pt.lon if pt.lon > @bounds.max_lon
82
+ @distance += haversine_distance(last_pt, pt) unless last_pt.nil?
83
+ @points << pt
84
+ end
85
+
86
+ # Returns true if the given time is within this Segment.
87
+ def contains_time?(time)
88
+ (time >= @earliest_point.time and time <= @latest_point.time)
89
+ end
90
+
91
+ # Finds the closest point in time to the passed-in time argument. Useful
92
+ # for matching up time-based objects (photos, video, etc) with a
93
+ # geographic location.
94
+ def closest_point(time)
95
+ find_closest(points, time)
96
+ end
97
+
98
+ # Deletes all points within this Segment that lie outside of the given
99
+ # area (which should be a Bounds object).
100
+ def crop(area)
101
+ delete_if { |pt| not area.contains?(pt) }
102
+ end
103
+
104
+ # Deletes all points in this Segment that lie within the given area.
105
+ def delete_area(area)
106
+ delete_if{ |pt| area.contains?(pt) }
107
+ end
108
+
109
+ # A handy method that deletes points based on a block that is passed in.
110
+ # If the passed-in block returns true when given a point, then that point
111
+ # is deleted. For example:
112
+ # delete_if{ |pt| area.contains?(pt) }
113
+ def delete_if
114
+ reset_meta_data
115
+ keep_points = []
116
+ last_pt = nil
117
+ points.each do |pt|
118
+ unless yield(pt)
119
+ keep_points << pt
120
+ update_meta_data(pt, last_pt)
121
+ last_pt = pt
122
+ end
123
+ end
124
+ @points = keep_points
125
+ end
126
+
127
+ # Returns true if this Segment has no points.
128
+ def empty?
129
+ (points.nil? or (points.size == 0))
130
+ end
131
+
132
+ # Converts this Segment to a REXML::Element object.
133
+ def to_xml
134
+ seg = Element.new('trkseg')
135
+ points.each { |pt| seg.elements << pt.to_xml }
136
+ seg
137
+ end
138
+
139
+ # Prints out a nice summary of this Segment.
140
+ def to_s
141
+ result = "Track Segment\n"
142
+ result << "\tSize: #{points.size} points\n"
143
+ result << "\tDistance: #{distance} km\n"
144
+ result << "\tEarliest Point: #{earliest_point.time.to_s} \n"
145
+ result << "\tLatest Point: #{latest_point.time.to_s} \n"
146
+ result << "\tLowest Point: #{lowest_point.elevation} \n"
147
+ result << "\tHighest Point: #{highest_point.elevation}\n "
148
+ result << "\tBounds: #{bounds.to_s}"
149
+ result
150
+ end
151
+
152
+ protected
153
+ def find_closest(pts, time)
154
+ return pts.first if pts.size == 1
155
+ midpoint = pts.size/2
156
+ if pts.size == 2
157
+ diff_1 = pts[0].time - time
158
+ diff_2 = pts[1].time - time
159
+ return (diff_1 < diff_2 ? pts[0] : pts[1])
160
+ end
161
+ if time >= pts[midpoint].time and time <= pts[midpoint+1].time
162
+
163
+ return pts[midpoint]
164
+
165
+ elsif(time <= pts[midpoint].time)
166
+ return find_closest(pts[0..midpoint], time)
167
+ else
168
+ return find_closest(pts[(midpoint+1)..-1], time)
169
+ end
170
+ end
171
+
172
+ RADIUS = 6371; # earth's mean radius in km
173
+
174
+ # Calculate the Haversine distance between two points. This is the method
175
+ # the library uses to calculate the cumulative distance of GPX files.
176
+ def haversine_distance(p1, p2)
177
+ d_lat = p2.latr - p1.latr;
178
+ d_lon = p2.lonr - p1.lonr;
179
+ a = Math.sin(d_lat/2) * Math.sin(d_lat/2) + Math.cos(p1.latr) * Math.cos(p2.latr) * Math.sin(d_lon/2) * Math.sin(d_lon/2);
180
+ c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
181
+ d = RADIUS * c;
182
+ return d;
183
+ end
184
+
185
+ # Calculate the plain Pythagorean difference between two points. Not currently used.
186
+ def pythagorean_distance(p1, p2)
187
+ Math.sqrt((p2.latr - p1.latr)**2 + (p2.lonr - p1.lonr)**2)
188
+ end
189
+
190
+ # Calculates the distance between two points using the Law of Cosines formula. Not currently used.
191
+ def law_of_cosines_distance(p1, p2)
192
+ (Math.acos(Math.sin(p1.latr)*Math.sin(p2.latr) + Math.cos(p1.latr)*Math.cos(p2.latr)*Math.cos(p2.lonr-p1.lonr)) * RADIUS)
193
+ end
194
+
195
+ def reset_meta_data
196
+ @earliest_point = nil
197
+ @latest_point = nil
198
+ @highest_point = nil
199
+ @lowest_point = nil
200
+ @distance = 0.0
201
+ @bounds = Bounds.new
202
+ end
203
+
204
+ def update_meta_data(pt, last_pt)
205
+ @earliest_point = pt if(@earliest_point.nil? or pt.time < @earliest_point.time)
206
+ @latest_point = pt if(@latest_point.nil? or pt.time > @latest_point.time)
207
+ unless pt.elevation.nil?
208
+ @lowest_point = pt if(@lowest_point.nil? or pt.elevation < @lowest_point.elevation)
209
+ @highest_point = pt if(@highest_point.nil? or pt.elevation > @highest_point.elevation)
210
+ end
211
+ @bounds.add(pt)
212
+ @distance += haversine_distance(last_pt, pt) unless last_pt.nil?
213
+ end
214
+
215
+ end
216
+
217
+ end