gpx 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +43 -0
- data/Rakefile +81 -0
- data/lib/gpx.rb +37 -0
- data/lib/gpx/bounds.rb +83 -0
- data/lib/gpx/gpx.rb +53 -0
- data/lib/gpx/gpx_file.rb +222 -0
- data/lib/gpx/magellan_track_log.rb +134 -0
- data/lib/gpx/point.rb +103 -0
- data/lib/gpx/route.rb +65 -0
- data/lib/gpx/segment.rb +217 -0
- data/lib/gpx/track.rb +149 -0
- data/lib/gpx/trackpoint.rb +35 -0
- data/lib/gpx/waypoint.rb +73 -0
- data/tests/gpx_files/arches.gpx +1 -0
- data/tests/gpx_files/magellan_track.log +306 -0
- data/tests/gpx_files/one_segment.gpx +770 -0
- data/tests/gpx_files/one_track.gpx +756 -0
- data/tests/gpx_files/routes.gpx +1 -0
- data/tests/gpx_files/tracks.gpx +6304 -0
- data/tests/gpx_files/waypoints.gpx +1 -0
- data/tests/magellan_test.rb +18 -0
- data/tests/segment_test.rb +57 -0
- data/tests/track_file_test.rb +75 -0
- data/tests/track_test.rb +65 -0
- metadata +70 -0
@@ -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
|
data/lib/gpx/segment.rb
ADDED
@@ -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
|