gpx_doctor 0.3.0 → 0.4.0
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.
- checksums.yaml +4 -4
- data/README.md +152 -1
- data/lib/gpx_doctor/configuration.rb +10 -1
- data/lib/gpx_doctor/cumulative_distance_enhancer.rb +28 -0
- data/lib/gpx_doctor/models/waypoint.rb +1 -1
- data/lib/gpx_doctor/parser.rb +9 -0
- data/lib/gpx_doctor/statistics_enhancer.rb +9 -5
- data/lib/gpx_doctor/unit_converter.rb +32 -0
- data/lib/gpx_doctor/version.rb +1 -1
- data/lib/gpx_doctor.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e0ba71f5752f36af3efd9f3b12de47d843a85b3ecd7fbcabd7468a2eef43355e
|
|
4
|
+
data.tar.gz: b58cb9eb1f207c3ec26f127b615ba43f70efe741904c6e73aa2c4ab059a7b710
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 053df672b67de8623f4749e7a5d0e8419667abd7227e31a8937d2434ebad3a0af73d04ae54af95112ecffe1e7824314634316cc1bc1793863d5c9289057cf4bd
|
|
7
|
+
data.tar.gz: 2376c74be6339f8aeeb535e8f79dbd9dd876bca7526713f9be8e09da3793718eaba4e285a455e4a07e1f182768affcf206c6ae7ba81483e37c6468e59bf96e27
|
data/README.md
CHANGED
|
@@ -24,6 +24,7 @@ GpxDoctor.configure do |config|
|
|
|
24
24
|
config.elevation_server_url = "https://elevation.example.com"
|
|
25
25
|
config.elevation_server_user = "user"
|
|
26
26
|
config.elevation_server_password = "secret"
|
|
27
|
+
config.unit_system = :imperial # or :metric (default)
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
GpxDoctor.configuration.elevation_server_url # => "https://elevation.example.com"
|
|
@@ -36,6 +37,33 @@ GpxDoctor.reset_configuration! # resets to defaults
|
|
|
36
37
|
| `elevation_server_url` | String | `nil` | URL of the elevation server |
|
|
37
38
|
| `elevation_server_user` | String | `nil` | Username for the elevation server |
|
|
38
39
|
| `elevation_server_password` | String | `nil` | Password for the elevation server |
|
|
40
|
+
| `unit_system` | Symbol | `:metric` | Unit system (`:metric` or `:imperial`) for distance and elevation values |
|
|
41
|
+
|
|
42
|
+
### Unit System
|
|
43
|
+
|
|
44
|
+
The `unit_system` configuration affects the following fields:
|
|
45
|
+
|
|
46
|
+
- **`:metric` (default)**:
|
|
47
|
+
- `distance_to_next` and `elevation_change` are in **meters**
|
|
48
|
+
- `cumulative_distance` is in **kilometers**
|
|
49
|
+
- **`:imperial`**:
|
|
50
|
+
- `distance_to_next` and `elevation_change` are in **feet**
|
|
51
|
+
- `cumulative_distance` is in **miles**
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
# Use imperial units
|
|
55
|
+
GpxDoctor.configure do |config|
|
|
56
|
+
config.unit_system = :imperial
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
result = GpxDoctor::Parser.parse("path/to/file.gpx", params: {
|
|
60
|
+
segment_statistics: true,
|
|
61
|
+
cumulative_distance: true
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
# distance_to_next and elevation_change will be in feet
|
|
65
|
+
# cumulative_distance will be in miles
|
|
66
|
+
```
|
|
39
67
|
|
|
40
68
|
## Parsing
|
|
41
69
|
|
|
@@ -60,6 +88,7 @@ result = GpxDoctor::Parser.parse("path/to/file.gpx", params: {
|
|
|
60
88
|
max_distance: 200, # insert interpolated points so no two consecutive points exceed this distance (metres)
|
|
61
89
|
max_points: 500, # reduce each segment to at most this many points
|
|
62
90
|
segment_statistics: true, # compute distance_to_next, elevation_change, direction for each point
|
|
91
|
+
cumulative_distance: true, # add cumulative distance from start of each segment/route
|
|
63
92
|
enhance_elevation: true # fetch missing elevations from the configured elevation server
|
|
64
93
|
})
|
|
65
94
|
```
|
|
@@ -69,10 +98,13 @@ Processing is applied in the following order:
|
|
|
69
98
|
1. `max_distance` — segment splitting (interpolates intermediate points)
|
|
70
99
|
2. `max_points` — point reduction
|
|
71
100
|
3. `segment_statistics` — per-point statistics (distance, bearing, elevation change)
|
|
72
|
-
4. `
|
|
101
|
+
4. `cumulative_distance` — cumulative distance from the start of each segment/route
|
|
102
|
+
5. `enhance_elevation` — elevation lookup via the elevation server
|
|
73
103
|
|
|
74
104
|
`enhance_elevation: true` requires the elevation server to be configured (see **Configuration** above). It only fills in points that have no elevation value; existing elevations are left unchanged.
|
|
75
105
|
|
|
106
|
+
`cumulative_distance: true` adds a `cumulative_distance` field to each point, representing the cumulative distance in kilometers from the start of its segment or route. For tracks with multiple segments, each segment's cumulative distance starts at 0.0 (gaps between segments are not included in the calculation).
|
|
107
|
+
|
|
76
108
|
## Accessing data
|
|
77
109
|
|
|
78
110
|
```ruby
|
|
@@ -88,6 +120,121 @@ result.metadata # => #<Metadata …> (or nil)
|
|
|
88
120
|
- `<rtept>` elements inside each `<rte>`
|
|
89
121
|
- `<trkpt>` elements inside each `<trkseg>` inside each `<trk>`
|
|
90
122
|
|
|
123
|
+
## Building GPX files
|
|
124
|
+
|
|
125
|
+
The `GpxDoctor::Builder` class generates GPX 1.1 XML from a `Result` object (the same structure returned by the parser).
|
|
126
|
+
|
|
127
|
+
### Build to a string
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
result = GpxDoctor::Parser::Result.new(
|
|
131
|
+
waypoints: [],
|
|
132
|
+
routes: [],
|
|
133
|
+
tracks: [],
|
|
134
|
+
metadata: nil
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
xml_string = GpxDoctor::Builder.build(result)
|
|
138
|
+
# => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<gpx version=\"1.1\"..."
|
|
139
|
+
|
|
140
|
+
# Optional: specify a custom creator attribute (identifies the software that created the GPX file)
|
|
141
|
+
xml_string = GpxDoctor::Builder.build(result, creator: 'My Application')
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Build to a file
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
GpxDoctor::Builder.build_file(result, 'path/to/output.gpx')
|
|
148
|
+
# Writes the GPX XML to the file and returns the XML string
|
|
149
|
+
|
|
150
|
+
# Optional: specify a custom creator attribute
|
|
151
|
+
GpxDoctor::Builder.build_file(result, 'path/to/output.gpx', creator: 'My Application')
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Input structure
|
|
155
|
+
|
|
156
|
+
The builder expects a `GpxDoctor::Parser::Result` object with the following fields:
|
|
157
|
+
|
|
158
|
+
- **`waypoints`** — Array of `GpxDoctor::Waypoint` objects (top-level waypoints)
|
|
159
|
+
- **`routes`** — Array of `GpxDoctor::Route` objects
|
|
160
|
+
- **`tracks`** — Array of `GpxDoctor::Track` objects
|
|
161
|
+
- **`metadata`** — `GpxDoctor::Metadata` object (optional)
|
|
162
|
+
|
|
163
|
+
All model classes are simple Ruby objects with attributes matching the GPX 1.1 specification (see **Model field reference** below).
|
|
164
|
+
|
|
165
|
+
### Example: Creating a GPX file from scratch
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
require 'gpx_doctor'
|
|
169
|
+
|
|
170
|
+
# Create waypoints
|
|
171
|
+
waypoint = GpxDoctor::Waypoint.new(
|
|
172
|
+
lat: 48.2093723,
|
|
173
|
+
lon: 16.356099,
|
|
174
|
+
ele: 160.0,
|
|
175
|
+
name: 'Vienna',
|
|
176
|
+
desc: 'Capital of Austria'
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Create a route with points
|
|
180
|
+
route_point_1 = GpxDoctor::Waypoint.new(lat: 48.21, lon: 16.36, ele: 155.0)
|
|
181
|
+
route_point_2 = GpxDoctor::Waypoint.new(lat: 48.22, lon: 16.37, ele: 162.0)
|
|
182
|
+
|
|
183
|
+
route = GpxDoctor::Route.new(
|
|
184
|
+
name: 'City Tour',
|
|
185
|
+
desc: 'A route through the city',
|
|
186
|
+
points: [route_point_1, route_point_2]
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Create a track with segments
|
|
190
|
+
track_point_1 = GpxDoctor::Waypoint.new(lat: 48.23, lon: 16.38, ele: 170.0)
|
|
191
|
+
track_point_2 = GpxDoctor::Waypoint.new(lat: 48.24, lon: 16.39, ele: 175.0)
|
|
192
|
+
|
|
193
|
+
segment = GpxDoctor::TrackSegment.new(points: [track_point_1, track_point_2])
|
|
194
|
+
track = GpxDoctor::Track.new(
|
|
195
|
+
name: 'Morning Run',
|
|
196
|
+
desc: 'My morning jog',
|
|
197
|
+
segments: [segment]
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Create metadata (optional)
|
|
201
|
+
metadata = GpxDoctor::Metadata.new(
|
|
202
|
+
name: 'My GPX File',
|
|
203
|
+
desc: 'A custom GPX file',
|
|
204
|
+
time: Time.now
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Build the result object
|
|
208
|
+
result = GpxDoctor::Parser::Result.new(
|
|
209
|
+
waypoints: [waypoint],
|
|
210
|
+
routes: [route],
|
|
211
|
+
tracks: [track],
|
|
212
|
+
metadata: metadata
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Generate GPX XML
|
|
216
|
+
xml_string = GpxDoctor::Builder.build(result, creator: 'My Application')
|
|
217
|
+
|
|
218
|
+
# Or write directly to a file
|
|
219
|
+
GpxDoctor::Builder.build_file(result, 'my_route.gpx', creator: 'My Application')
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Round-trip workflow
|
|
223
|
+
|
|
224
|
+
You can parse an existing GPX file, modify it, and build it back:
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
# Parse existing file
|
|
228
|
+
result = GpxDoctor::Parser.parse('input.gpx')
|
|
229
|
+
|
|
230
|
+
# Modify data
|
|
231
|
+
result.routes.first.name = 'Updated Route Name'
|
|
232
|
+
result.waypoints << GpxDoctor::Waypoint.new(lat: 48.5, lon: 16.5, name: 'New Point')
|
|
233
|
+
|
|
234
|
+
# Build back to GPX
|
|
235
|
+
GpxDoctor::Builder.build_file(result, 'output.gpx')
|
|
236
|
+
```
|
|
237
|
+
|
|
91
238
|
## Model field reference
|
|
92
239
|
|
|
93
240
|
### `Waypoint`
|
|
@@ -114,6 +261,10 @@ result.metadata # => #<Metadata …> (or nil)
|
|
|
114
261
|
| `pdop` | Float | |
|
|
115
262
|
| `ageofdgpsdata` | Float | |
|
|
116
263
|
| `dgpsid` | Integer | 0–1023 |
|
|
264
|
+
| `distance_to_next` | Float | Distance to next point (metres or feet based on `unit_system`) — set by `segment_statistics: true` |
|
|
265
|
+
| `elevation_change` | Float | Elevation change to next point (metres or feet based on `unit_system`) — set by `segment_statistics: true` |
|
|
266
|
+
| `direction` | Float | Bearing to next point (0–360°) — set by `segment_statistics: true` |
|
|
267
|
+
| `cumulative_distance` | Float | Cumulative distance from segment/route start (kilometres or miles based on `unit_system`) — set by `cumulative_distance: true` |
|
|
117
268
|
|
|
118
269
|
`Waypoint#to_h` returns a hash of all non-nil fields.
|
|
119
270
|
|
|
@@ -5,13 +5,22 @@ module GpxDoctor
|
|
|
5
5
|
attr_accessor :elevation_server,
|
|
6
6
|
:elevation_server_url,
|
|
7
7
|
:elevation_server_user,
|
|
8
|
-
:elevation_server_password
|
|
8
|
+
:elevation_server_password,
|
|
9
|
+
:unit_system
|
|
9
10
|
|
|
10
11
|
def initialize
|
|
11
12
|
@elevation_server = false
|
|
12
13
|
@elevation_server_url = nil
|
|
13
14
|
@elevation_server_user = nil
|
|
14
15
|
@elevation_server_password = nil
|
|
16
|
+
@unit_system = :metric
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def unit_system=(value)
|
|
20
|
+
unless [:metric, :imperial].include?(value)
|
|
21
|
+
raise ArgumentError, "unit_system must be :metric or :imperial"
|
|
22
|
+
end
|
|
23
|
+
@unit_system = value
|
|
15
24
|
end
|
|
16
25
|
end
|
|
17
26
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GpxDoctor
|
|
4
|
+
class CumulativeDistanceEnhancer
|
|
5
|
+
# Enhances each waypoint with cumulative distance from the start of the segment/route.
|
|
6
|
+
# The first point receives cumulative_distance = 0.0
|
|
7
|
+
# Each subsequent point receives cumulative_distance = previous.cumulative_distance + distance from previous
|
|
8
|
+
# Distance is stored in kilometers for metric and miles for imperial.
|
|
9
|
+
#
|
|
10
|
+
# Mutates waypoints in place.
|
|
11
|
+
def enhance(waypoints)
|
|
12
|
+
return if waypoints.nil? || waypoints.empty?
|
|
13
|
+
|
|
14
|
+
unit_system = GpxDoctor.configuration.unit_system
|
|
15
|
+
|
|
16
|
+
cumulative = 0.0
|
|
17
|
+
waypoints.first.cumulative_distance = cumulative
|
|
18
|
+
|
|
19
|
+
waypoints.each_cons(2) do |current, nxt|
|
|
20
|
+
distance_meters = DistanceCalculator.distance(current, nxt)
|
|
21
|
+
distance_km = distance_meters / 1000.0
|
|
22
|
+
distance_converted = UnitConverter.convert_cumulative_distance(distance_km, unit_system)
|
|
23
|
+
cumulative += distance_converted
|
|
24
|
+
nxt.cumulative_distance = cumulative
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -8,7 +8,7 @@ module GpxDoctor
|
|
|
8
8
|
sym type fix sat hdop vdop pdop ageofdgpsdata dgpsid
|
|
9
9
|
].freeze
|
|
10
10
|
|
|
11
|
-
STATISTICS_FIELDS = %i[distance_to_next elevation_change direction].freeze
|
|
11
|
+
STATISTICS_FIELDS = %i[distance_to_next elevation_change direction cumulative_distance].freeze
|
|
12
12
|
|
|
13
13
|
attr_accessor(*STATISTICS_FIELDS)
|
|
14
14
|
|
data/lib/gpx_doctor/parser.rb
CHANGED
|
@@ -53,6 +53,7 @@ module GpxDoctor
|
|
|
53
53
|
split_segments(result, eff_max_dist) if eff_max_dist
|
|
54
54
|
select_max_points(result) if @params[:max_points]
|
|
55
55
|
enhance_statistics(result) if @params[:segment_statistics]
|
|
56
|
+
enhance_cumulative_distance(result) if @params[:cumulative_distance]
|
|
56
57
|
enhance_elevations(result) if @params[:enhance_elevation]
|
|
57
58
|
|
|
58
59
|
result
|
|
@@ -267,5 +268,13 @@ module GpxDoctor
|
|
|
267
268
|
track.segments.each { |seg| enhancer.enhance(seg.points) }
|
|
268
269
|
end
|
|
269
270
|
end
|
|
271
|
+
|
|
272
|
+
def enhance_cumulative_distance(result)
|
|
273
|
+
enhancer = CumulativeDistanceEnhancer.new
|
|
274
|
+
result.routes.each { |route| enhancer.enhance(route.points) }
|
|
275
|
+
result.tracks.each do |track|
|
|
276
|
+
track.segments.each { |seg| enhancer.enhance(seg.points) }
|
|
277
|
+
end
|
|
278
|
+
end
|
|
270
279
|
end
|
|
271
280
|
end
|
|
@@ -3,20 +3,24 @@
|
|
|
3
3
|
module GpxDoctor
|
|
4
4
|
class StatisticsEnhancer
|
|
5
5
|
# Enhances each consecutive pair of waypoints with statistics:
|
|
6
|
-
# - distance_to_next (meters
|
|
7
|
-
# - elevation_change
|
|
8
|
-
# - direction
|
|
6
|
+
# - distance_to_next (meters or feet depending on configuration)
|
|
7
|
+
# - elevation_change (meters or feet depending on configuration)
|
|
8
|
+
# - direction (degrees 0-360, geographic bearing to next point)
|
|
9
9
|
#
|
|
10
10
|
# The last point in the list receives nil for all three fields.
|
|
11
11
|
# Mutates waypoints in place.
|
|
12
12
|
def enhance(waypoints)
|
|
13
13
|
return if waypoints.nil? || waypoints.size < 2
|
|
14
14
|
|
|
15
|
+
unit_system = GpxDoctor.configuration.unit_system
|
|
16
|
+
|
|
15
17
|
waypoints.each_cons(2) do |current, nxt|
|
|
16
|
-
|
|
18
|
+
distance_meters = DistanceCalculator.distance(current, nxt)
|
|
19
|
+
current.distance_to_next = UnitConverter.convert_distance(distance_meters, unit_system)
|
|
17
20
|
|
|
18
21
|
current.elevation_change = if current.ele && nxt.ele
|
|
19
|
-
nxt.ele - current.ele
|
|
22
|
+
elevation_change_meters = nxt.ele - current.ele
|
|
23
|
+
UnitConverter.convert_elevation(elevation_change_meters, unit_system)
|
|
20
24
|
end
|
|
21
25
|
|
|
22
26
|
current.direction = DistanceCalculator.bearing(current, nxt)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module GpxDoctor
|
|
4
|
+
module UnitConverter
|
|
5
|
+
METERS_TO_FEET = 3.28084
|
|
6
|
+
METERS_TO_MILES = 0.000621371
|
|
7
|
+
KILOMETERS_TO_MILES = 0.621371
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Convert meters to the configured unit system
|
|
12
|
+
# Returns feet for imperial, meters for metric
|
|
13
|
+
def convert_distance(meters, unit_system = :metric)
|
|
14
|
+
return meters if unit_system == :metric
|
|
15
|
+
meters * METERS_TO_FEET
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Convert meters to the configured unit system
|
|
19
|
+
# Returns feet for imperial, meters for metric
|
|
20
|
+
def convert_elevation(meters, unit_system = :metric)
|
|
21
|
+
return meters if unit_system == :metric
|
|
22
|
+
meters * METERS_TO_FEET
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Convert kilometers to the configured unit system
|
|
26
|
+
# Returns miles for imperial, kilometers for metric
|
|
27
|
+
def convert_cumulative_distance(kilometers, unit_system = :metric)
|
|
28
|
+
return kilometers if unit_system == :metric
|
|
29
|
+
kilometers * KILOMETERS_TO_MILES
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/gpx_doctor/version.rb
CHANGED
data/lib/gpx_doctor.rb
CHANGED
|
@@ -15,8 +15,10 @@ require 'gpx_doctor/models/track_segment'
|
|
|
15
15
|
require 'gpx_doctor/models/route'
|
|
16
16
|
require 'gpx_doctor/models/track'
|
|
17
17
|
require 'gpx_doctor/distance_calculator'
|
|
18
|
+
require 'gpx_doctor/unit_converter'
|
|
18
19
|
require 'gpx_doctor/elevation_client'
|
|
19
20
|
require 'gpx_doctor/statistics_enhancer'
|
|
21
|
+
require 'gpx_doctor/cumulative_distance_enhancer'
|
|
20
22
|
require 'gpx_doctor/segment_splitter'
|
|
21
23
|
require 'gpx_doctor/point_selector'
|
|
22
24
|
require 'gpx_doctor/parser'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gpx_doctor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Poltrax
|
|
@@ -47,6 +47,7 @@ files:
|
|
|
47
47
|
- lib/gpx_doctor.rb
|
|
48
48
|
- lib/gpx_doctor/builder.rb
|
|
49
49
|
- lib/gpx_doctor/configuration.rb
|
|
50
|
+
- lib/gpx_doctor/cumulative_distance_enhancer.rb
|
|
50
51
|
- lib/gpx_doctor/distance_calculator.rb
|
|
51
52
|
- lib/gpx_doctor/elevation_client.rb
|
|
52
53
|
- lib/gpx_doctor/errors.rb
|
|
@@ -65,6 +66,7 @@ files:
|
|
|
65
66
|
- lib/gpx_doctor/point_selector.rb
|
|
66
67
|
- lib/gpx_doctor/segment_splitter.rb
|
|
67
68
|
- lib/gpx_doctor/statistics_enhancer.rb
|
|
69
|
+
- lib/gpx_doctor/unit_converter.rb
|
|
68
70
|
- lib/gpx_doctor/validator.rb
|
|
69
71
|
- lib/gpx_doctor/version.rb
|
|
70
72
|
homepage: https://github.com/Poltrax-live/gpx-doctor
|