gpx 0.8.3 → 1.1.1
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 +37 -0
- data/.gitignore +4 -0
- data/.rubocop +1 -0
- data/.rubocop.yml +162 -0
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/.travis.yml +4 -6
- data/CHANGELOG.md +32 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +1 -1
- data/README.md +38 -17
- data/Rakefile +22 -12
- data/UPGRADING.md +7 -0
- data/bin/gpx_distance +5 -6
- data/bin/gpx_smooth +25 -26
- data/gpx.gemspec +14 -11
- 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 +45 -13
- data/lib/gpx/trackpoint.rb +0 -60
data/lib/gpx/bounds.rb
CHANGED
@@ -1,58 +1,41 @@
|
|
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
|
class Bounds < Base
|
25
|
-
attr_accessor :min_lat, :max_lat, :max_lon, :min_lon
|
5
|
+
attr_accessor :min_lat, :max_lat, :max_lon, :min_lon
|
26
6
|
|
27
7
|
# Creates a new bounds object with the passed-in min and max longitudes
|
28
8
|
# and latitudes.
|
29
|
-
def initialize(opts = { :
|
30
|
-
|
31
|
-
@
|
9
|
+
def initialize(opts = { min_lat: 90.0, max_lat: -90.0, min_lon: 180.0, max_lon: -180.0 })
|
10
|
+
super()
|
11
|
+
@min_lat = opts[:min_lat].to_f
|
12
|
+
@max_lat = opts[:max_lat].to_f
|
13
|
+
@min_lon = opts[:min_lon].to_f
|
14
|
+
@max_lon = opts[:max_lon].to_f
|
32
15
|
end
|
33
16
|
|
34
17
|
# Returns the middle latitude.
|
35
18
|
def center_lat
|
36
|
-
distance = (max_lat - min_lat)/2.0
|
19
|
+
distance = (max_lat - min_lat) / 2.0
|
37
20
|
(min_lat + distance)
|
38
21
|
end
|
39
22
|
|
40
23
|
# Returns the middle longitude.
|
41
24
|
def center_lon
|
42
|
-
distance = (max_lon - min_lon)/2.0
|
25
|
+
distance = (max_lon - min_lon) / 2.0
|
43
26
|
(min_lon + distance)
|
44
27
|
end
|
45
28
|
|
46
29
|
# Returns true if the pt is within these bounds.
|
47
30
|
def contains?(pt)
|
48
|
-
(pt.lat >=
|
31
|
+
((pt.lat >= min_lat) && (pt.lat <= max_lat) && (pt.lon >= min_lon) && (pt.lon <= max_lon))
|
49
32
|
end
|
50
33
|
|
51
34
|
# Adds an item to itself, expanding its min/max lat/lon as needed to
|
52
35
|
# contain the given item. The item can be either another instance of
|
53
36
|
# Bounds or a Point.
|
54
37
|
def add(item)
|
55
|
-
if
|
38
|
+
if item.respond_to?(:lat) && item.respond_to?(:lon)
|
56
39
|
@min_lat = item.lat if item.lat < @min_lat
|
57
40
|
@min_lon = item.lon if item.lon < @min_lon
|
58
41
|
@max_lat = item.lat if item.lat > @max_lat
|
@@ -69,6 +52,5 @@ module GPX
|
|
69
52
|
def to_s
|
70
53
|
"min_lat: #{min_lat} min_lon: #{min_lon} max_lat: #{max_lat} max_lon: #{max_lon}"
|
71
54
|
end
|
72
|
-
|
73
55
|
end
|
74
56
|
end
|
data/lib/gpx/geo_json.rb
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module GPX
|
6
|
+
# Class to parse GeoJSON LineStrings, MultiLineStrings, Points,
|
7
|
+
# and MultiPoint geometric objects to GPX format. For the full
|
8
|
+
# specification of GeoJSON, see:
|
9
|
+
# http://geojson.org/geojson-spec.html
|
10
|
+
# Note that GeoJSON coordinates are specified in lon/lat format,
|
11
|
+
# instead of the more traditional lat/lon format.
|
12
|
+
#
|
13
|
+
class GeoJSON
|
14
|
+
class << self
|
15
|
+
FEATURE = 'Feature'
|
16
|
+
LINESTRING = 'LineString'
|
17
|
+
MULTILINESTRING = 'MultiLineString'
|
18
|
+
POINT = 'Point'
|
19
|
+
MULTIPOINT = 'MultiPoint'
|
20
|
+
|
21
|
+
# Conversion can be initiated by either specifying a file,
|
22
|
+
# file name, or by passing in GeoJSON data as a string.
|
23
|
+
# Examples:
|
24
|
+
# GPX::GeoJSON.convert_to_gpx(geojson_file: 'mygeojsonfile.json')
|
25
|
+
# or
|
26
|
+
# file = File.new('mygeojsonfile.json', 'r')
|
27
|
+
# GPX::GeoJSON.convert_to_gpx(geojson_file: file)
|
28
|
+
# or
|
29
|
+
# data = JSON.generate(my_geojson_hash)
|
30
|
+
# GPX::GeoJSON.convert_to_gpx(geojson_data: data)
|
31
|
+
#
|
32
|
+
# Returns a GPX::GPX_File object populated with the converted data.
|
33
|
+
#
|
34
|
+
def convert_to_gpx(opts = {})
|
35
|
+
geojson = geojson_data_from(opts)
|
36
|
+
gpx_file = GPX::GPXFile.new
|
37
|
+
add_tracks_to(gpx_file, geojson)
|
38
|
+
add_waypoints_to(gpx_file, geojson)
|
39
|
+
gpx_file
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def geojson_data_from(opts)
|
45
|
+
if opts[:geojson_file]
|
46
|
+
parse_geojson_data_from_file(opts[:geojson_file])
|
47
|
+
elsif opts[:geojson_data]
|
48
|
+
parse_geojson_data(opts[:geojson_data])
|
49
|
+
else
|
50
|
+
raise ArgumentError,
|
51
|
+
'Must pass value for \':geojson_file\' ' \
|
52
|
+
'or \':geojson_data\' to convert_to_gpx'
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def parse_geojson_data_from_file(filename)
|
57
|
+
parse_geojson_data(IO.read(filename))
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_geojson_data(data)
|
61
|
+
JSON.parse(data)
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_tracks_to(gpx_file, geojson)
|
65
|
+
tracks = [line_strings_to_track(geojson)] +
|
66
|
+
multi_line_strings_to_tracks(geojson)
|
67
|
+
tracks.compact!
|
68
|
+
gpx_file.tracks += tracks
|
69
|
+
gpx_file.tracks.each { |t| gpx_file.update_meta_data(t) }
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_waypoints_to(gpx_file, geojson)
|
73
|
+
gpx_file.waypoints +=
|
74
|
+
points_to_waypoints(geojson, gpx_file) +
|
75
|
+
multi_points_to_waypoints(geojson, gpx_file)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Converts GeoJSON 'LineString' features.
|
79
|
+
# Current strategy is to convert each LineString into a
|
80
|
+
# Track Segment, returning a Track for all LineStrings.
|
81
|
+
#
|
82
|
+
def line_strings_to_track(geojson)
|
83
|
+
line_strings = line_strings_in(geojson)
|
84
|
+
return nil unless line_strings.any?
|
85
|
+
|
86
|
+
track = GPX::Track.new
|
87
|
+
line_strings.each do |ls|
|
88
|
+
coords = ls['geometry']['coordinates']
|
89
|
+
track.append_segment(coords_to_segment(coords))
|
90
|
+
end
|
91
|
+
track
|
92
|
+
end
|
93
|
+
|
94
|
+
# Converts GeoJSON 'MultiLineString' features.
|
95
|
+
# Current strategy is to convert each MultiLineString
|
96
|
+
# into a Track, with each set of LineString coordinates
|
97
|
+
# within a MultiLineString a Track Segment.
|
98
|
+
#
|
99
|
+
def multi_line_strings_to_tracks(geojson)
|
100
|
+
tracks = []
|
101
|
+
multi_line_strings_in(geojson).each do |mls|
|
102
|
+
track = GPX::Track.new
|
103
|
+
mls['geometry']['coordinates'].each do |coords|
|
104
|
+
seg = coords_to_segment(coords)
|
105
|
+
seg.track = track
|
106
|
+
track.append_segment(seg)
|
107
|
+
end
|
108
|
+
tracks << track
|
109
|
+
end
|
110
|
+
tracks
|
111
|
+
end
|
112
|
+
|
113
|
+
# Converts GeoJSON 'Point' features.
|
114
|
+
# Current strategy is to convert each Point
|
115
|
+
# feature into a GPX waypoint.
|
116
|
+
#
|
117
|
+
def points_to_waypoints(geojson, gpx_file)
|
118
|
+
points_in(geojson).reduce([]) do |acc, pt|
|
119
|
+
coords = pt['geometry']['coordinates']
|
120
|
+
acc << point_to_waypoint(coords, gpx_file)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Converts GeoJSON 'MultiPoint' features.
|
125
|
+
# Current strategy is to convert each coordinate
|
126
|
+
# point in a MultiPoint to a GPX waypoint.
|
127
|
+
#
|
128
|
+
# NOTE: It is debatable that a MultiPoint feature
|
129
|
+
# might translate best into a GPX route, which is
|
130
|
+
# described as
|
131
|
+
# "an ordered list of waypoints representing a
|
132
|
+
# series of turn points leading to a destination."
|
133
|
+
# See http://www.topografix.com/gpx/1/1/#type_rteType
|
134
|
+
#
|
135
|
+
def multi_points_to_waypoints(geojson, gpx_file)
|
136
|
+
multi_points_in(geojson).reduce([]) do |acc, mpt|
|
137
|
+
mpt['geometry']['coordinates'].each do |coords|
|
138
|
+
acc << point_to_waypoint(coords, gpx_file)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Given an array of [lng, lat, ele] coordinates,
|
144
|
+
# return a GPX track segment.
|
145
|
+
#
|
146
|
+
def coords_to_segment(coords)
|
147
|
+
seg = GPX::Segment.new
|
148
|
+
coords.each do |pt|
|
149
|
+
seg.append_point(point_to_track_point(pt, seg))
|
150
|
+
end
|
151
|
+
seg
|
152
|
+
end
|
153
|
+
|
154
|
+
# Given a GeoJSON coordinate point, return
|
155
|
+
# a GPX::Waypoint
|
156
|
+
def point_to_waypoint(point, gpx_file)
|
157
|
+
GPX::Waypoint.new(gpx_file: gpx_file,
|
158
|
+
lon: point[0],
|
159
|
+
lat: point[1],
|
160
|
+
elevation: point[2])
|
161
|
+
end
|
162
|
+
|
163
|
+
# Given a GeoJSON coorindate point, and
|
164
|
+
# GPX segment, return a GPX::TrackPoint.
|
165
|
+
#
|
166
|
+
def point_to_track_point(point, seg)
|
167
|
+
GPX::TrackPoint.new(segment: seg,
|
168
|
+
lon: point[0],
|
169
|
+
lat: point[1],
|
170
|
+
elevation: point[2])
|
171
|
+
end
|
172
|
+
|
173
|
+
# Returns all features in the passed geojson
|
174
|
+
# that match the type.
|
175
|
+
#
|
176
|
+
def features_for(geojson, type)
|
177
|
+
geojson['features'].find_all do |f|
|
178
|
+
f['type'] == FEATURE && f['geometry']['type'] == type
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def points_in(geojson)
|
183
|
+
features_for(geojson, POINT)
|
184
|
+
end
|
185
|
+
|
186
|
+
def multi_points_in(geojson)
|
187
|
+
features_for(geojson, MULTIPOINT)
|
188
|
+
end
|
189
|
+
|
190
|
+
def line_strings_in(geojson)
|
191
|
+
features_for(geojson, LINESTRING)
|
192
|
+
end
|
193
|
+
|
194
|
+
def multi_line_strings_in(geojson)
|
195
|
+
features_for(geojson, MULTILINESTRING)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/lib/gpx/gpx.rb
CHANGED
@@ -1,25 +1,5 @@
|
|
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 common base class which provides a useful initializer method to many
|
25
5
|
# class in the GPX library.
|
@@ -33,14 +13,12 @@ module GPX
|
|
33
13
|
# attributes to this method.
|
34
14
|
def instantiate_with_text_elements(parent, text_elements)
|
35
15
|
text_elements.each do |el|
|
36
|
-
child_xpath =
|
16
|
+
child_xpath = el.to_s
|
37
17
|
unless parent.at(child_xpath).nil?
|
38
18
|
val = parent.at(child_xpath).inner_text
|
39
|
-
|
19
|
+
send("#{el}=", val)
|
40
20
|
end
|
41
21
|
end
|
42
|
-
|
43
22
|
end
|
44
|
-
|
45
23
|
end
|
46
24
|
end
|