gpx 0.1

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,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