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