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 +4 -4
- data/README.md +37 -2
- data/lib/gpx_doctor/models/waypoint.rb +1 -1
- data/lib/gpx_doctor/parser.rb +58 -1
- data/lib/gpx_doctor/point_labeler.rb +94 -0
- data/lib/gpx_doctor/version.rb +1 -1
- data/lib/gpx_doctor.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: acb87257e118bff8499cfa14686bd88d04c6b89beb02b0f5c50bff087a73fd8e
|
|
4
|
+
data.tar.gz: 6ecc166553da8067c1970fd22452aae97384ebdd36363f661ccbf8cf54cea5bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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. `
|
|
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
|
|
data/lib/gpx_doctor/parser.rb
CHANGED
|
@@ -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
|
data/lib/gpx_doctor/version.rb
CHANGED
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
|
+
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
|