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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 367e09fc58af88bd756703fd5e2647632fd9991f753f3581e3bcb439f7733ce3
4
- data.tar.gz: 3a7922964bb774be1eb1a1d2af78aee4a7d56b868b7652c481fb186cc6438bce
3
+ metadata.gz: e0ba71f5752f36af3efd9f3b12de47d843a85b3ecd7fbcabd7468a2eef43355e
4
+ data.tar.gz: b58cb9eb1f207c3ec26f127b615ba43f70efe741904c6e73aa2c4ab059a7b710
5
5
  SHA512:
6
- metadata.gz: 9bad3d27880e55e001fea734b7d481f19569f0e0d202958d4ae893a1722eef5b0fb7a5431030f970872358c812e5a83a492f99c945096652e9547f0945238624
7
- data.tar.gz: 0a4a032459ed9916bc8cd99c64d38676822efbed6e1a470d86a9806878f3ee65f5239827f1830eeb6dbdac573f845a64bcea70b7f24f5b686bc8ef440fb29325
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. `enhance_elevation` — elevation lookup via the elevation server
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
 
@@ -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, flat-earth Pythagorean approximation)
7
- # - elevation_change (meters, next.ele - current.ele; nil when elevation missing)
8
- # - direction (degrees 0-360, geographic bearing to next point)
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
- current.distance_to_next = DistanceCalculator.distance(current, nxt)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GpxDoctor
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end
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.3.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