gpx_doctor 0.4.0 → 0.6.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: e0ba71f5752f36af3efd9f3b12de47d843a85b3ecd7fbcabd7468a2eef43355e
4
- data.tar.gz: b58cb9eb1f207c3ec26f127b615ba43f70efe741904c6e73aa2c4ab059a7b710
3
+ metadata.gz: acb87257e118bff8499cfa14686bd88d04c6b89beb02b0f5c50bff087a73fd8e
4
+ data.tar.gz: 6ecc166553da8067c1970fd22452aae97384ebdd36363f661ccbf8cf54cea5bd
5
5
  SHA512:
6
- metadata.gz: 053df672b67de8623f4749e7a5d0e8419667abd7227e31a8937d2434ebad3a0af73d04ae54af95112ecffe1e7824314634316cc1bc1793863d5c9289057cf4bd
7
- data.tar.gz: 2376c74be6339f8aeeb535e8f79dbd9dd876bca7526713f9be8e09da3793718eaba4e285a455e4a07e1f182768affcf206c6ae7ba81483e37c6468e59bf96e27
6
+ metadata.gz: 61092aea3f7b2b2995289ada84eead6566b24ede417b0d916802c18ec9f463fd9b444d0515610bb73bb9c05e80f012be50111927e72b081721e927fa139993c5
7
+ data.tar.gz: 00d4010f2c0f85d1fc3773d5e6da614944205f8278099370dd2f846a471bf06c9a7cafb689f75ddd12d0c74b56b0de27fc99410769de4370b35e07dc7b224fae
data/README.md CHANGED
@@ -89,7 +89,9 @@ result = GpxDoctor::Parser.parse("path/to/file.gpx", params: {
89
89
  max_points: 500, # reduce each segment to at most this many points
90
90
  segment_statistics: true, # compute distance_to_next, elevation_change, direction for each point
91
91
  cumulative_distance: true, # add cumulative distance from start of each segment/route
92
- enhance_elevation: true # fetch missing elevations from the configured elevation server
92
+ label_interval: 1.0, # insert an interpolated, labelled point at every interval mark (kilometres or miles based on unit_system)
93
+ enhance_elevation: true, # fetch missing elevations from the configured elevation server
94
+ full_poi_data: true # populate result.pois with start/finish boundary data for the whole GPX and each track segment
93
95
  })
94
96
  ```
95
97
 
@@ -99,12 +101,18 @@ Processing is applied in the following order:
99
101
  2. `max_points` — point reduction
100
102
  3. `segment_statistics` — per-point statistics (distance, bearing, elevation change)
101
103
  4. `cumulative_distance` — cumulative distance from the start of each segment/route
102
- 5. `enhance_elevation` — elevation lookup via the elevation server
104
+ 5. `label_interval` — labelled point insertion
105
+ 6. `enhance_elevation` — elevation lookup via the elevation server
106
+ 7. `full_poi_data` — POI boundary extraction (start, finish, and per-segment boundaries)
103
107
 
104
108
  `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.
105
109
 
106
110
  `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
111
 
112
+ `label_interval: 1.0` walks each route/segment and inserts an interpolated point at every multiple of the given interval (measured as cumulative distance from the start, in kilometres for `:metric` or miles for `:imperial` — see **Unit System** above), setting the new point's `label` field to that distance (e.g. `1.0` for the point at the 1.0 km/mi mark). A mark falling on an existing point (within a small tolerance) is skipped rather than duplicated. `label_interval` runs after `cumulative_distance`, reusing its `cumulative_distance` values instead of recalculating them when `cumulative_distance: true` is also given. **Because `label_interval` is applied after `max_points`, the resulting number of points may exceed `max_points`.**
113
+
114
+ `full_poi_data: true` populates `result.pois` with the first and last geographic point of the entire GPX (across all routes and track segments), plus optional per-segment boundary data. Distances within each segment start at 0.0 and reflect that segment's length only. The `ele` key is omitted for points that have no elevation value. The global `finish` distance is the sum of all individual collection lengths. See **`result.pois`** below for the output shape.
115
+
108
116
  ## Accessing data
109
117
 
110
118
  ```ruby
@@ -113,6 +121,7 @@ result.waypoints # => [#<Waypoint …>] (top-level <wpt> elements only)
113
121
  result.routes # => [#<Route …>]
114
122
  result.tracks # => [#<Track …>]
115
123
  result.metadata # => #<Metadata …> (or nil)
124
+ result.pois # => Hash (only when parsed with full_poi_data: true, otherwise nil)
116
125
  ```
117
126
 
118
127
  `result.points` is a flat array containing **all** geographic points from:
@@ -120,6 +129,31 @@ result.metadata # => #<Metadata …> (or nil)
120
129
  - `<rtept>` elements inside each `<rte>`
121
130
  - `<trkpt>` elements inside each `<trkseg>` inside each `<trk>`
122
131
 
132
+ ### `result.pois`
133
+
134
+ When parsed with `full_poi_data: true`, `result.pois` contains boundary data for the entire GPX and each track segment:
135
+
136
+ ```ruby
137
+ result = GpxDoctor::Parser.parse("route.gpx", params: { full_poi_data: true })
138
+ result.pois
139
+ # =>
140
+ # {
141
+ # start: { lon: 16.38, lat: 48.23, ele: 170.0, distance: 0.0 },
142
+ # finish: { lon: 16.39, lat: 48.24, ele: 175.0, distance: 1.42 },
143
+ # segments: [
144
+ # {
145
+ # start: { lon: 16.38, lat: 48.23, ele: 170.0, distance: 0.0 },
146
+ # finish: { lon: 16.39, lat: 48.24, ele: 175.0, distance: 1.42 }
147
+ # }
148
+ # ]
149
+ # }
150
+ ```
151
+
152
+ - **`start`** / **`finish`** — first and last geographic point across all routes and track segments, each with `lon`, `lat`, `distance` (and `ele` when present). `start` always has `distance: 0.0`; `finish` distance is the sum of all individual route/segment lengths.
153
+ - **`segments`** — present only when the GPX contains track segments. Each entry is `{start:, finish:}` for one `<trkseg>`, with distances measured from the beginning of that segment (`start` is always `distance: 0.0`).
154
+ - The `ele` key is omitted for points that have no elevation value.
155
+ - Distance values respect the configured `unit_system` (kilometres for `:metric`, miles for `:imperial`).
156
+
123
157
  ## Building GPX files
124
158
 
125
159
  The `GpxDoctor::Builder` class generates GPX 1.1 XML from a `Result` object (the same structure returned by the parser).
@@ -265,6 +299,7 @@ GpxDoctor::Builder.build_file(result, 'output.gpx')
265
299
  | `elevation_change` | Float | Elevation change to next point (metres or feet based on `unit_system`) — set by `segment_statistics: true` |
266
300
  | `direction` | Float | Bearing to next point (0–360°) — set by `segment_statistics: true` |
267
301
  | `cumulative_distance` | Float | Cumulative distance from segment/route start (kilometres or miles based on `unit_system`) — set by `cumulative_distance: true` |
302
+ | `label` | Float | Cumulative distance mark for a point inserted by `label_interval` (kilometres or miles based on `unit_system`); `nil` for points not created for labelling — set by `label_interval` |
268
303
 
269
304
  `Waypoint#to_h` returns a hash of all non-nil fields.
270
305
 
@@ -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 cumulative_distance].freeze
11
+ STATISTICS_FIELDS = %i[distance_to_next elevation_change direction cumulative_distance label].freeze
12
12
 
13
13
  attr_accessor(*STATISTICS_FIELDS)
14
14
 
@@ -7,7 +7,7 @@ module GpxDoctor
7
7
  class Parser
8
8
  GPX_NS = 'http://www.topografix.com/GPX/1/1'
9
9
 
10
- Result = Struct.new(:waypoints, :routes, :tracks, :metadata, keyword_init: true) do
10
+ Result = Struct.new(:waypoints, :routes, :tracks, :metadata, :pois, keyword_init: true) do
11
11
  def points
12
12
  waypoints + routes.flat_map(&:points) + tracks.flat_map(&:points)
13
13
  end
@@ -54,7 +54,9 @@ module GpxDoctor
54
54
  select_max_points(result) if @params[:max_points]
55
55
  enhance_statistics(result) if @params[:segment_statistics]
56
56
  enhance_cumulative_distance(result) if @params[:cumulative_distance]
57
+ label_points(result) if @params[:label_interval]
57
58
  enhance_elevations(result) if @params[:enhance_elevation]
59
+ result.pois = build_pois(result) if @params[:full_poi_data]
58
60
 
59
61
  result
60
62
  end
@@ -276,5 +278,60 @@ module GpxDoctor
276
278
  track.segments.each { |seg| enhancer.enhance(seg.points) }
277
279
  end
278
280
  end
281
+
282
+ def label_points(result)
283
+ labeler = PointLabeler.new
284
+ interval = @params[:label_interval]
285
+ result.routes.each { |route| route.points = labeler.label(route.points, interval) }
286
+ result.tracks.each do |track|
287
+ track.segments.each { |seg| seg.points = labeler.label(seg.points, interval) }
288
+ end
289
+ end
290
+
291
+ def point_to_poi(point, distance)
292
+ poi = { lon: point.lon, lat: point.lat }
293
+ poi[:ele] = point.ele if point.ele
294
+ poi[:distance] = distance
295
+ poi
296
+ end
297
+
298
+ def build_pois(result)
299
+ collections = result.routes.map(&:points) +
300
+ result.tracks.flat_map { |t| t.segments.map(&:points) }
301
+ non_empty = collections.reject(&:empty?)
302
+
303
+ return {} if non_empty.empty?
304
+
305
+ total_distance = non_empty.sum { |pts| collection_distance(pts) }
306
+
307
+ pois = {
308
+ start: point_to_poi(non_empty.first.first, 0.0),
309
+ finish: point_to_poi(non_empty.last.last, total_distance)
310
+ }
311
+
312
+ segments_pois = result.tracks.flat_map do |track|
313
+ track.segments.filter_map do |seg|
314
+ next if seg.points.empty?
315
+
316
+ {
317
+ start: point_to_poi(seg.points.first, 0.0),
318
+ finish: point_to_poi(seg.points.last, collection_distance(seg.points))
319
+ }
320
+ end
321
+ end
322
+
323
+ pois[:segments] = segments_pois unless segments_pois.empty?
324
+ pois
325
+ end
326
+
327
+ def collection_distance(points)
328
+ return 0.0 if points.length < 2
329
+
330
+ unit_system = GpxDoctor.configuration.unit_system
331
+ points.each_cons(2).sum do |a, b|
332
+ dist_km = DistanceCalculator.distance(a, b) / 1000.0
333
+ UnitConverter.convert_cumulative_distance(dist_km, unit_system)
334
+ end
335
+ end
279
336
  end
280
337
  end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GpxDoctor
4
+ class PointLabeler
5
+ # Tolerance (in the configured distance unit) used to decide whether a
6
+ # label distance coincides with an existing point, to avoid inserting a
7
+ # near-duplicate point right next to it.
8
+ EPSILON = 1e-9
9
+
10
+ # Inserts an interpolated point at every multiple of +label_interval+
11
+ # measured as cumulative distance from the start of +points+ (kilometres
12
+ # for :metric, miles for :imperial — see GpxDoctor.configuration.unit_system).
13
+ #
14
+ # When +points+ already carry a +cumulative_distance+ (set by
15
+ # GpxDoctor::CumulativeDistanceEnhancer), those values are reused instead
16
+ # of being recalculated. Otherwise cumulative distance is computed from
17
+ # scratch.
18
+ #
19
+ # Existing points are left untouched. A new point is inserted between the
20
+ # two existing points that bracket each label distance, with lat/lon
21
+ # always interpolated, and ele/time interpolated only when both endpoints
22
+ # have values. The new point's +label+ field is set to the target
23
+ # distance (e.g. 1.0 for the point at the 1.0 km/mi mark).
24
+ #
25
+ # A label distance that falls (within a small tolerance) on an existing
26
+ # point is skipped — no duplicate point is inserted.
27
+ #
28
+ # Returns a new array; the original is not mutated. When +points+ has
29
+ # fewer than 2 elements, or +label_interval+ is nil, zero or negative,
30
+ # the original array is returned unchanged.
31
+ def label(points, label_interval)
32
+ return points if points.nil? || points.size < 2 || label_interval.nil? || label_interval <= 0
33
+
34
+ cumulative = cumulative_distances(points)
35
+
36
+ result = [points.first]
37
+ target = label_interval
38
+
39
+ points.each_cons(2).with_index do |(a, b), i|
40
+ start_dist = cumulative[i]
41
+ end_dist = cumulative[i + 1]
42
+
43
+ while target <= end_dist + EPSILON
44
+ if target > start_dist + EPSILON && target < end_dist - EPSILON
45
+ result << interpolate(a, b, start_dist, end_dist, target)
46
+ end
47
+ target += label_interval
48
+ end
49
+
50
+ result << b
51
+ end
52
+
53
+ result
54
+ end
55
+
56
+ private
57
+
58
+ # Returns the cumulative distance of every point, reusing each point's
59
+ # already-computed +cumulative_distance+ (see CumulativeDistanceEnhancer)
60
+ # when available, to avoid walking the points and recalculating distances
61
+ # a second time.
62
+ def cumulative_distances(points)
63
+ return points.map(&:cumulative_distance) if precomputed?(points)
64
+
65
+ unit_system = GpxDoctor.configuration.unit_system
66
+
67
+ cumulative = [0.0]
68
+ points.each_cons(2) do |a, b|
69
+ distance_km = DistanceCalculator.distance(a, b) / 1000.0
70
+ distance_converted = UnitConverter.convert_cumulative_distance(distance_km, unit_system)
71
+ cumulative << cumulative.last + distance_converted
72
+ end
73
+ cumulative
74
+ end
75
+
76
+ def precomputed?(points)
77
+ points.all? { |p| p.respond_to?(:cumulative_distance) && !p.cumulative_distance.nil? }
78
+ end
79
+
80
+ def interpolate(a, b, start_dist, end_dist, target)
81
+ fraction = (target - start_dist) / (end_dist - start_dist)
82
+ ele = a.ele && b.ele ? a.ele + fraction * (b.ele - a.ele) : nil
83
+ time = a.time && b.time ? a.time + fraction * (b.time - a.time) : nil
84
+
85
+ Models::Waypoint.new(
86
+ lat: a.lat + fraction * (b.lat - a.lat),
87
+ lon: a.lon + fraction * (b.lon - a.lon),
88
+ ele: ele,
89
+ time: time,
90
+ label: target
91
+ )
92
+ end
93
+ end
94
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GpxDoctor
4
- VERSION = '0.4.0'
4
+ VERSION = '0.6.0'
5
5
  end
data/lib/gpx_doctor.rb CHANGED
@@ -21,6 +21,7 @@ require 'gpx_doctor/statistics_enhancer'
21
21
  require 'gpx_doctor/cumulative_distance_enhancer'
22
22
  require 'gpx_doctor/segment_splitter'
23
23
  require 'gpx_doctor/point_selector'
24
+ require 'gpx_doctor/point_labeler'
24
25
  require 'gpx_doctor/parser'
25
26
  require 'gpx_doctor/builder'
26
27
  require 'gpx_doctor/geojson_builder'
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.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Poltrax
@@ -63,6 +63,7 @@ files:
63
63
  - lib/gpx_doctor/models/track_segment.rb
64
64
  - lib/gpx_doctor/models/waypoint.rb
65
65
  - lib/gpx_doctor/parser.rb
66
+ - lib/gpx_doctor/point_labeler.rb
66
67
  - lib/gpx_doctor/point_selector.rb
67
68
  - lib/gpx_doctor/segment_splitter.rb
68
69
  - lib/gpx_doctor/statistics_enhancer.rb