gpx 0.9.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/ruby.yml +36 -0
- data/.gitignore +4 -0
- data/.rubocop.yml +162 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -5
- data/CHANGELOG.md +15 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +20 -5
- data/Rakefile +22 -12
- data/bin/gpx_distance +5 -6
- data/bin/gpx_smooth +25 -26
- data/gpx.gemspec +13 -12
- data/lib/gpx/bounds.rb +13 -31
- data/lib/gpx/geo_json.rb +199 -0
- data/lib/gpx/gpx.rb +4 -26
- data/lib/gpx/gpx_file.rb +140 -134
- data/lib/gpx/magellan_track_log.rb +34 -66
- data/lib/gpx/point.rb +22 -35
- data/lib/gpx/route.rb +10 -31
- data/lib/gpx/segment.rb +63 -90
- data/lib/gpx/track.rb +38 -42
- data/lib/gpx/track_point.rb +32 -0
- data/lib/gpx/version.rb +3 -1
- data/lib/gpx/waypoint.rb +10 -34
- data/lib/gpx.rb +13 -34
- data/tests/geojson_files/combined_data.json +68 -0
- data/tests/geojson_files/line_string_data.json +83 -0
- data/tests/geojson_files/multi_line_string_data.json +74 -0
- data/tests/geojson_files/multi_point_data.json +14 -0
- data/tests/geojson_files/point_data.json +22 -0
- data/tests/geojson_test.rb +92 -0
- data/tests/gpx10_test.rb +7 -6
- data/tests/gpx_file_test.rb +31 -19
- data/tests/gpx_files/one_segment_mixed_times.gpx +884 -0
- data/tests/gpx_files/routes_without_names.gpx +29 -0
- data/tests/gpx_files/with_empty_tracks.gpx +72 -0
- data/tests/magellan_test.rb +12 -11
- data/tests/output_test.rb +93 -94
- data/tests/route_test.rb +75 -30
- data/tests/segment_test.rb +104 -93
- data/tests/track_file_test.rb +50 -70
- data/tests/track_point_test.rb +22 -11
- data/tests/track_test.rb +73 -61
- data/tests/waypoint_test.rb +46 -48
- metadata +47 -18
- data/lib/gpx/trackpoint.rb +0 -60
@@ -1,30 +1,10 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
#++
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
23
3
|
module GPX
|
24
4
|
# This class will parse the lat/lon and time data from a Magellan track log,
|
25
5
|
# which is a NMEA formatted CSV list of points.
|
26
6
|
class MagellanTrackLog
|
27
|
-
#PMGNTRK
|
7
|
+
# PMGNTRK
|
28
8
|
# This message is to be used to transmit Track information (basically a list of previous position fixes)
|
29
9
|
# which is often displayed on the plotter or map screen of the unit. The first field in this message
|
30
10
|
# is the Latitude, followed by N or S. The next field is the Longitude followed by E or W. The next
|
@@ -35,98 +15,86 @@ module GPX
|
|
35
15
|
# of the fix. Note that this field is (and its preceding comma) is only produced by the unit when the
|
36
16
|
# command PMGNCMD,TRACK,2 is given. It is not present when a simple command of PMGNCMD,TRACK is issued.
|
37
17
|
|
38
|
-
#NOTE: The Latitude and Longitude Fields are shown as having two decimal
|
18
|
+
# NOTE: The Latitude and Longitude Fields are shown as having two decimal
|
39
19
|
# places. As many additional decimal places may be added as long as the total
|
40
20
|
# length of the message does not exceed 82 bytes.
|
41
21
|
|
42
22
|
# $PMGNTRK,llll.ll,a,yyyyy.yy,a,xxxxx,a,hhmmss.ss,A,c----c,ddmmyy*hh<CR><LF>
|
43
23
|
require 'csv'
|
44
24
|
|
45
|
-
LAT
|
46
|
-
LAT_HEMI
|
47
|
-
LON
|
48
|
-
LON_HEMI
|
49
|
-
ELE
|
25
|
+
LAT = 1
|
26
|
+
LAT_HEMI = 2
|
27
|
+
LON = 3
|
28
|
+
LON_HEMI = 4
|
29
|
+
ELE = 5
|
50
30
|
ELE_UNITS = 6
|
51
|
-
TIME
|
31
|
+
TIME = 7
|
52
32
|
INVALID_FLAG = 8
|
53
|
-
DATE
|
33
|
+
DATE = 10
|
54
34
|
|
55
35
|
FEET_TO_METERS = 0.3048
|
56
36
|
|
57
37
|
class << self
|
58
|
-
|
59
38
|
# Takes the name of a magellan file, converts the contents to GPX, and
|
60
39
|
# writes the result to gpx_filename.
|
61
40
|
def convert_to_gpx(magellan_filename, gpx_filename)
|
62
|
-
|
63
41
|
segment = Segment.new
|
64
42
|
|
65
|
-
CSV.open(magellan_filename,
|
66
|
-
next if(row.size < 10
|
43
|
+
CSV.open(magellan_filename, 'r').each do |row|
|
44
|
+
next if (row.size < 10) || (row[INVALID_FLAG] == 'V')
|
67
45
|
|
68
|
-
lat_deg
|
69
|
-
lat_min
|
46
|
+
lat_deg = row[LAT][0..1]
|
47
|
+
lat_min = row[LAT][2...-1]
|
70
48
|
lat_hemi = row[LAT_HEMI]
|
71
49
|
|
72
50
|
lat = lat_deg.to_f + (lat_min.to_f / 60.0)
|
73
|
-
lat =
|
51
|
+
lat = -lat if lat_hemi == 'S'
|
74
52
|
|
75
|
-
lon_deg
|
76
|
-
lon_min
|
53
|
+
lon_deg = row[LON][0..2]
|
54
|
+
lon_min = row[LON][3..-1]
|
77
55
|
lon_hemi = row[LON_HEMI]
|
78
56
|
|
79
57
|
lon = lon_deg.to_f + (lon_min.to_f / 60.0)
|
80
|
-
lon =
|
81
|
-
|
58
|
+
lon = -lon if lon_hemi == 'W'
|
82
59
|
|
83
60
|
ele = row[ELE]
|
84
61
|
ele_units = row[ELE_UNITS]
|
85
62
|
ele = ele.to_f
|
86
|
-
if
|
87
|
-
ele *= FEET_TO_METERS
|
88
|
-
end
|
63
|
+
ele *= FEET_TO_METERS if ele_units == 'F'
|
89
64
|
|
90
|
-
hrs
|
65
|
+
hrs = row[TIME][0..1].to_i
|
91
66
|
mins = row[TIME][2..3].to_i
|
92
67
|
secs = row[TIME][4..5].to_i
|
93
|
-
day
|
94
|
-
mon
|
95
|
-
yr
|
68
|
+
day = row[DATE][0..1].to_i
|
69
|
+
mon = row[DATE][2..3].to_i
|
70
|
+
yr = 2000 + row[DATE][4..5].to_i
|
96
71
|
|
97
72
|
time = Time.gm(yr, mon, day, hrs, mins, secs)
|
98
73
|
|
99
|
-
#must create point
|
100
|
-
pt = TrackPoint.new(:
|
74
|
+
# must create point
|
75
|
+
pt = TrackPoint.new(lat: lat, lon: lon, time: time, elevation: ele)
|
101
76
|
segment.append_point(pt)
|
102
|
-
|
103
77
|
end
|
104
78
|
|
105
79
|
trk = Track.new
|
106
80
|
trk.append_segment(segment)
|
107
|
-
gpx_file = GPXFile.new(:
|
81
|
+
gpx_file = GPXFile.new(tracks: [trk])
|
108
82
|
gpx_file.write(gpx_filename)
|
109
|
-
|
110
83
|
end
|
111
84
|
|
112
85
|
# Tests to see if the given file is a magellan NMEA track log.
|
113
|
-
def
|
86
|
+
def magellan_file?(filename)
|
114
87
|
i = 0
|
115
|
-
File.open(filename,
|
88
|
+
File.open(filename, 'r') do |f|
|
116
89
|
f.each do |line|
|
117
|
-
i +=
|
118
|
-
if line =~ /^\$PMGNTRK/
|
119
|
-
|
120
|
-
|
121
|
-
return false
|
122
|
-
elsif(i > 10)
|
123
|
-
return false
|
124
|
-
end
|
90
|
+
i += 1
|
91
|
+
return true if line =~ /^\$PMGNTRK/
|
92
|
+
return false if line =~ /<\?xml/
|
93
|
+
return false if i > 10
|
125
94
|
end
|
126
95
|
end
|
127
|
-
|
96
|
+
false
|
128
97
|
end
|
129
98
|
end
|
130
|
-
|
131
99
|
end
|
132
100
|
end
|
data/lib/gpx/point.rb
CHANGED
@@ -1,46 +1,35 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
#++
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
23
3
|
module GPX
|
24
4
|
# The base class for all points. Trackpoint and Waypoint both descend from this base class.
|
25
5
|
class Point < Base
|
26
|
-
D_TO_R = Math::PI/180.0
|
27
|
-
attr_accessor :
|
6
|
+
D_TO_R = Math::PI / 180.0
|
7
|
+
attr_accessor :time, :elevation, :gpx_file, :speed, :extensions
|
8
|
+
attr_reader :lat, :lon
|
28
9
|
|
29
10
|
# When you need to manipulate individual points, you can create a Point
|
30
11
|
# object with a latitude, a longitude, an elevation, and a time. In
|
31
12
|
# addition, you can pass an XML element to this initializer, and the
|
32
13
|
# relevant info will be parsed out.
|
33
|
-
def initialize(opts = {:
|
14
|
+
def initialize(opts = { lat: 0.0, lon: 0.0, elevation: 0.0, time: Time.now })
|
15
|
+
super()
|
34
16
|
@gpx_file = opts[:gpx_file]
|
35
|
-
if
|
17
|
+
if opts[:element]
|
36
18
|
elem = opts[:element]
|
37
|
-
@lat
|
38
|
-
@
|
39
|
-
|
40
|
-
@
|
41
|
-
|
42
|
-
@
|
43
|
-
|
19
|
+
@lat = elem['lat'].to_f
|
20
|
+
@lon = elem['lon'].to_f
|
21
|
+
@latr = (D_TO_R * @lat)
|
22
|
+
@lonr = (D_TO_R * @lon)
|
23
|
+
# '-'? yyyy '-' mm '-' dd 'T' hh ':' mm ':' ss ('.' s+)? (zzzzzz)?
|
24
|
+
@time = (
|
25
|
+
begin
|
26
|
+
Time.xmlschema(elem.at('time').inner_text)
|
27
|
+
rescue StandardError
|
28
|
+
nil
|
29
|
+
end)
|
30
|
+
@elevation = elem.at('ele').inner_text.to_f unless elem.at('ele').nil?
|
31
|
+
@speed = elem.at('speed').inner_text.to_f unless elem.at('speed').nil?
|
32
|
+
@extensions = elem.at('extensions') unless elem.at('extensions').nil?
|
44
33
|
else
|
45
34
|
@lat = opts[:lat]
|
46
35
|
@lon = opts[:lon]
|
@@ -49,10 +38,8 @@ module GPX
|
|
49
38
|
@speed = opts[:speed]
|
50
39
|
@extensions = opts[:extensions]
|
51
40
|
end
|
52
|
-
|
53
41
|
end
|
54
42
|
|
55
|
-
|
56
43
|
# Returns the latitude and longitude (in that order), separated by the
|
57
44
|
# given delimeter. This is useful for passing a point into another API
|
58
45
|
# (i.e. the Google Maps javascript API).
|
data/lib/gpx/route.rb
CHANGED
@@ -1,58 +1,37 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
#++
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
23
3
|
module GPX
|
24
4
|
# A Route in GPX is very similar to a Track, but it is created by a user
|
25
5
|
# from a series of Waypoints, whereas a Track is created by the GPS device
|
26
6
|
# automatically logging your progress at regular intervals.
|
27
7
|
class Route < Base
|
28
|
-
|
29
8
|
attr_accessor :points, :name, :gpx_file
|
30
9
|
|
31
10
|
# Initialize a Route from a XML::Node.
|
32
11
|
def initialize(opts = {})
|
33
|
-
|
12
|
+
super()
|
13
|
+
if opts[:gpx_file] && opts[:element]
|
34
14
|
rte_element = opts[:element]
|
35
15
|
@gpx_file = opts[:gpx_file]
|
36
|
-
@name = rte_element.at(
|
16
|
+
@name = rte_element.at('name')&.inner_text
|
37
17
|
@points = []
|
38
|
-
rte_element.search(
|
39
|
-
@points << Point.new(:
|
18
|
+
rte_element.search('rtept').each do |point|
|
19
|
+
@points << Point.new(element: point, gpx_file: @gpx_file)
|
40
20
|
end
|
41
21
|
else
|
42
|
-
@points = (opts[:points]
|
22
|
+
@points = (opts[:points] || [])
|
43
23
|
@name = (opts[:name])
|
44
24
|
end
|
45
|
-
|
46
25
|
end
|
47
26
|
|
48
27
|
# Delete points outside of a given area.
|
49
28
|
def crop(area)
|
50
|
-
points.delete_if{ |pt|
|
29
|
+
points.delete_if { |pt| !area.contains? pt }
|
51
30
|
end
|
52
31
|
|
53
32
|
# Delete points within the given area.
|
54
33
|
def delete_area(area)
|
55
|
-
points.delete_if{ |pt| area.contains? pt }
|
34
|
+
points.delete_if { |pt| area.contains? pt }
|
56
35
|
end
|
57
36
|
end
|
58
37
|
end
|
data/lib/gpx/segment.rb
CHANGED
@@ -1,38 +1,18 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
#++
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
23
3
|
module GPX
|
24
4
|
# A segment is the basic container in a GPX file. A Segment contains points
|
25
5
|
# (in this lib, they're called TrackPoints). A Track contains Segments. An
|
26
6
|
# instance of Segment knows its highest point, lowest point, earliest and
|
27
7
|
# latest points, distance, and bounds.
|
28
8
|
class Segment < Base
|
29
|
-
|
30
9
|
attr_reader :earliest_point, :latest_point, :bounds, :highest_point, :lowest_point, :distance, :duration
|
31
10
|
attr_accessor :points, :track
|
32
11
|
|
33
12
|
# If a XML::Node object is passed-in, this will initialize a new
|
34
13
|
# Segment based on its contents. Otherwise, a blank Segment is created.
|
35
14
|
def initialize(opts = {})
|
15
|
+
super()
|
36
16
|
@gpx_file = opts[:gpx_file]
|
37
17
|
@track = opts[:track]
|
38
18
|
@points = []
|
@@ -43,15 +23,13 @@ module GPX
|
|
43
23
|
@distance = 0.0
|
44
24
|
@duration = 0.0
|
45
25
|
@bounds = Bounds.new
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
end
|
54
|
-
end
|
26
|
+
|
27
|
+
segment_element = opts[:element]
|
28
|
+
return unless segment_element.is_a?(Nokogiri::XML::Node)
|
29
|
+
|
30
|
+
segment_element.search('trkpt').each do |trkpt|
|
31
|
+
pt = TrackPoint.new(element: trkpt, segment: self, gpx_file: @gpx_file)
|
32
|
+
append_point(pt)
|
55
33
|
end
|
56
34
|
end
|
57
35
|
|
@@ -59,8 +37,8 @@ module GPX
|
|
59
37
|
def append_point(pt)
|
60
38
|
last_pt = @points[-1]
|
61
39
|
if pt.time
|
62
|
-
@earliest_point = pt if
|
63
|
-
@latest_point
|
40
|
+
@earliest_point = pt if @earliest_point.nil? || (@earliest_point.time && pt.time < @earliest_point.time)
|
41
|
+
@latest_point = pt if @latest_point.nil? || (@latest_point.time && pt.time > @latest_point.time)
|
64
42
|
else
|
65
43
|
# when no time information in data, we consider the points are ordered
|
66
44
|
@earliest_point = @points[0]
|
@@ -68,8 +46,8 @@ module GPX
|
|
68
46
|
end
|
69
47
|
|
70
48
|
if pt.elevation
|
71
|
-
@lowest_point
|
72
|
-
@highest_point
|
49
|
+
@lowest_point = pt if @lowest_point.nil? || (pt.elevation < @lowest_point.elevation)
|
50
|
+
@highest_point = pt if @highest_point.nil? || (pt.elevation > @highest_point.elevation)
|
73
51
|
end
|
74
52
|
@bounds.min_lat = pt.lat if pt.lat < @bounds.min_lat
|
75
53
|
@bounds.min_lon = pt.lon if pt.lon < @bounds.min_lon
|
@@ -77,14 +55,16 @@ module GPX
|
|
77
55
|
@bounds.max_lon = pt.lon if pt.lon > @bounds.max_lon
|
78
56
|
if last_pt
|
79
57
|
@distance += haversine_distance(last_pt, pt)
|
80
|
-
@duration += pt.time - last_pt.time if pt.time
|
58
|
+
@duration += pt.time - last_pt.time if pt.time && last_pt.time
|
81
59
|
end
|
82
60
|
@points << pt
|
83
61
|
end
|
84
62
|
|
85
63
|
# Returns true if the given time is within this Segment.
|
86
64
|
def contains_time?(time)
|
87
|
-
(time >= @earliest_point.time
|
65
|
+
((time >= @earliest_point.time) && (time <= @latest_point.time))
|
66
|
+
rescue StandardError
|
67
|
+
false
|
88
68
|
end
|
89
69
|
|
90
70
|
# Finds the closest point in time to the passed-in time argument. Useful
|
@@ -97,12 +77,12 @@ module GPX
|
|
97
77
|
# Deletes all points within this Segment that lie outside of the given
|
98
78
|
# area (which should be a Bounds object).
|
99
79
|
def crop(area)
|
100
|
-
delete_if { |pt|
|
80
|
+
delete_if { |pt| !area.contains?(pt) }
|
101
81
|
end
|
102
82
|
|
103
83
|
# Deletes all points in this Segment that lie within the given area.
|
104
84
|
def delete_area(area)
|
105
|
-
delete_if{ |pt| area.contains?(pt) }
|
85
|
+
delete_if { |pt| area.contains?(pt) }
|
106
86
|
end
|
107
87
|
|
108
88
|
# A handy method that deletes points based on a block that is passed in.
|
@@ -114,18 +94,18 @@ module GPX
|
|
114
94
|
keep_points = []
|
115
95
|
last_pt = nil
|
116
96
|
points.each do |pt|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
97
|
+
next if yield(pt)
|
98
|
+
|
99
|
+
keep_points << pt
|
100
|
+
update_meta_data(pt, last_pt)
|
101
|
+
last_pt = pt
|
122
102
|
end
|
123
103
|
@points = keep_points
|
124
104
|
end
|
125
105
|
|
126
106
|
# Returns true if this Segment has no points.
|
127
107
|
def empty?
|
128
|
-
(points.nil?
|
108
|
+
(points.nil? || points.empty?)
|
129
109
|
end
|
130
110
|
|
131
111
|
# Prints out a nice summary of this Segment.
|
@@ -133,31 +113,31 @@ module GPX
|
|
133
113
|
result = "Track Segment\n"
|
134
114
|
result << "\tSize: #{points.size} points\n"
|
135
115
|
result << "\tDistance: #{distance} km\n"
|
136
|
-
result << "\tEarliest Point: #{earliest_point.time
|
137
|
-
result << "\tLatest Point: #{latest_point.time
|
116
|
+
result << "\tEarliest Point: #{earliest_point.time} \n"
|
117
|
+
result << "\tLatest Point: #{latest_point.time} \n"
|
138
118
|
result << "\tLowest Point: #{lowest_point.elevation} \n"
|
139
119
|
result << "\tHighest Point: #{highest_point.elevation}\n "
|
140
|
-
result << "\tBounds: #{bounds
|
120
|
+
result << "\tBounds: #{bounds}"
|
141
121
|
result
|
142
122
|
end
|
143
123
|
|
144
124
|
def find_point_by_time_or_offset(indicator)
|
145
125
|
if indicator.nil?
|
146
|
-
|
126
|
+
nil
|
147
127
|
elsif indicator.is_a?(Integer)
|
148
|
-
|
149
|
-
elsif
|
150
|
-
|
128
|
+
closest_point(@earliest_point.time + indicator)
|
129
|
+
elsif indicator.is_a?(Time)
|
130
|
+
closest_point(indicator)
|
151
131
|
else
|
152
|
-
raise
|
132
|
+
raise ArgumentError, 'find_end_point_by_time_or_offset requires an argument of type Time or Integer'
|
153
133
|
end
|
154
134
|
end
|
155
|
-
|
135
|
+
|
156
136
|
# smooths the location data in the segment (by recalculating the location as an average of 20 neighbouring points. Useful for removing noise from GPS traces.
|
157
|
-
def smooth_location_by_average(opts={})
|
137
|
+
def smooth_location_by_average(opts = {})
|
158
138
|
seconds_either_side = opts[:averaging_window] || 20
|
159
139
|
|
160
|
-
#calculate the first and last points to which the smoothing should be applied
|
140
|
+
# calculate the first and last points to which the smoothing should be applied
|
161
141
|
earliest = (find_point_by_time_or_offset(opts[:start]) || @earliest_point).time
|
162
142
|
latest = (find_point_by_time_or_offset(opts[:end]) || @latest_point).time
|
163
143
|
|
@@ -165,18 +145,18 @@ module GPX
|
|
165
145
|
|
166
146
|
@points.each do |point|
|
167
147
|
if point.time > latest || point.time < earliest
|
168
|
-
tmp_points.push point #add the point unaltered
|
169
|
-
next
|
148
|
+
tmp_points.push point # add the point unaltered
|
149
|
+
next
|
170
150
|
end
|
171
151
|
lat_av = 0.to_f
|
172
152
|
lon_av = 0.to_f
|
173
153
|
alt_av = 0.to_f
|
174
154
|
n = 0
|
175
|
-
# k ranges from the time of the current point +/- 20s
|
176
|
-
(-1*seconds_either_side..seconds_either_side).each do |k|
|
155
|
+
# k ranges from the time of the current point +/- 20s
|
156
|
+
(-1 * seconds_either_side..seconds_either_side).each do |k|
|
177
157
|
# find the point nearest to the time offset indicated by k
|
178
158
|
contributing_point = closest_point(point.time + k)
|
179
|
-
#sum up the contributions to the average
|
159
|
+
# sum up the contributions to the average
|
180
160
|
lat_av += contributing_point.lat
|
181
161
|
lon_av += contributing_point.lon
|
182
162
|
alt_av += contributing_point.elevation
|
@@ -184,37 +164,36 @@ module GPX
|
|
184
164
|
end
|
185
165
|
# calculate the averages
|
186
166
|
tmp_point = point.clone
|
187
|
-
tmp_point.lon = (
|
188
|
-
tmp_point.elevation = (
|
189
|
-
tmp_point.lat = (
|
167
|
+
tmp_point.lon = (lon_av / n).round(7)
|
168
|
+
tmp_point.elevation = (alt_av / n).round(2)
|
169
|
+
tmp_point.lat = (lat_av / n).round(7)
|
190
170
|
tmp_points.push tmp_point
|
191
171
|
end
|
192
|
-
last_pt = nil
|
193
172
|
@points.clear
|
194
173
|
reset_meta_data
|
195
|
-
#now commit the averages back and recalculate the distances
|
174
|
+
# now commit the averages back and recalculate the distances
|
196
175
|
tmp_points.each do |point|
|
197
176
|
append_point(point)
|
198
177
|
end
|
199
178
|
end
|
200
179
|
|
201
180
|
protected
|
181
|
+
|
202
182
|
def find_closest(pts, time)
|
203
183
|
return pts.first if pts.size == 1
|
204
|
-
|
184
|
+
|
185
|
+
midpoint = pts.size / 2
|
205
186
|
if pts.size == 2
|
206
187
|
diff_1 = pts[0].time - time
|
207
188
|
diff_2 = pts[1].time - time
|
208
189
|
return (diff_1 < diff_2 ? pts[0] : pts[1])
|
209
190
|
end
|
210
|
-
if time >= pts[midpoint].time
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
elsif(time <= pts[midpoint].time)
|
215
|
-
return find_closest(pts[0..midpoint], time)
|
191
|
+
if (time >= pts[midpoint].time) && (time <= pts[midpoint + 1].time)
|
192
|
+
pts[midpoint]
|
193
|
+
elsif time <= pts[midpoint].time
|
194
|
+
find_closest(pts[0..midpoint], time)
|
216
195
|
else
|
217
|
-
|
196
|
+
find_closest(pts[(midpoint + 1)..-1], time)
|
218
197
|
end
|
219
198
|
end
|
220
199
|
|
@@ -224,11 +203,6 @@ module GPX
|
|
224
203
|
p1.haversine_distance_from(p2)
|
225
204
|
end
|
226
205
|
|
227
|
-
# Calculate the plain Pythagorean difference between two points. Not currently used.
|
228
|
-
def pythagorean_distance(p1, p2)
|
229
|
-
p1.pythagorean_distance_from(p2)
|
230
|
-
end
|
231
|
-
|
232
206
|
# Calculates the distance between two points using the Law of Cosines formula. Not currently used.
|
233
207
|
def law_of_cosines_distance(p1, p2)
|
234
208
|
p1.law_of_cosines_distance_from(p2)
|
@@ -246,8 +220,8 @@ module GPX
|
|
246
220
|
|
247
221
|
def update_meta_data(pt, last_pt)
|
248
222
|
if pt.time
|
249
|
-
@earliest_point = pt if
|
250
|
-
@latest_point
|
223
|
+
@earliest_point = pt if @earliest_point.nil? || (pt.time < @earliest_point.time)
|
224
|
+
@latest_point = pt if @latest_point.nil? || (pt.time > @latest_point.time)
|
251
225
|
else
|
252
226
|
# when no time information in data, we consider the points are ordered
|
253
227
|
@earliest_point = @points[0]
|
@@ -255,16 +229,15 @@ module GPX
|
|
255
229
|
end
|
256
230
|
|
257
231
|
if pt.elevation
|
258
|
-
@lowest_point
|
259
|
-
@highest_point
|
232
|
+
@lowest_point = pt if @lowest_point.nil? || (pt.elevation < @lowest_point.elevation)
|
233
|
+
@highest_point = pt if @highest_point.nil? || (pt.elevation > @highest_point.elevation)
|
260
234
|
end
|
261
235
|
@bounds.add(pt)
|
262
|
-
if last_pt
|
263
|
-
@distance += haversine_distance(last_pt, pt)
|
264
|
-
@duration += pt.time - last_pt.time if pt.time and last_pt.time
|
265
|
-
end
|
266
|
-
end
|
267
236
|
|
268
|
-
|
237
|
+
return unless last_pt
|
269
238
|
|
239
|
+
@distance += haversine_distance(last_pt, pt)
|
240
|
+
@duration += pt.time - last_pt.time if pt.time && last_pt.time
|
241
|
+
end
|
242
|
+
end
|
270
243
|
end
|