GPX Doctor
A Ruby gem for parsing and manipulating GPX 1.1 routes.
Installation
Add to your Gemfile:
gem "gpx_doctor"
Or install directly:
gem install gpx_doctor
Configuration
GpxDoctor.configure do |config|
config.elevation_server = true
config.elevation_server_url = "https://elevation.example.com"
config.elevation_server_user = "user"
config.elevation_server_password = "secret"
config.unit_system = :imperial # or :metric (default)
end
GpxDoctor.configuration.elevation_server_url # => "https://elevation.example.com"
GpxDoctor.reset_configuration! # resets to defaults
| Option | Type | Default | Description |
|---|---|---|---|
elevation_server |
Boolean | false |
Whether to use an elevation server |
elevation_server_url |
String | nil |
URL of the elevation server |
elevation_server_user |
String | nil |
Username for the elevation server |
elevation_server_password |
String | nil |
Password for the elevation server |
unit_system |
Symbol | :metric |
Unit system (:metric or :imperial) for distance and elevation values |
Unit System
The unit_system configuration affects the following fields:
-
:metric(default):distance_to_nextandelevation_changeare in meterscumulative_distanceis in kilometers
-
:imperial:distance_to_nextandelevation_changeare in feetcumulative_distanceis in miles
# Use imperial units
GpxDoctor.configure do |config|
config.unit_system = :imperial
end
result = GpxDoctor::Parser.parse("path/to/file.gpx", params: {
segment_statistics: true,
cumulative_distance: true
})
# distance_to_next and elevation_change will be in feet
# cumulative_distance will be in miles
Parsing
From a file
result = GpxDoctor::Parser.parse("path/to/file.gpx")
From a string
result = GpxDoctor::Parser.parse_string(xml_string)
Parsing with processing parameters
Both parse and parse_string accept an optional params: hash to enable post-processing:
result = GpxDoctor::Parser.parse("path/to/file.gpx", params: {
max_distance: 200, # insert interpolated points so no two consecutive points exceed this distance (metres)
max_points: 500, # reduce each segment to at most this many points
segment_statistics: true, # compute distance_to_next, elevation_change, direction for each point
cumulative_distance: true, # add cumulative distance from start of each segment/route
enhance_elevation: true, # fetch missing elevations from the configured elevation server
full_poi_data: true # populate result.pois with start/finish boundary data for the whole GPX and each track segment
})
Processing is applied in the following order:
max_distance— segment splitting (interpolates intermediate points)max_points— point reductionsegment_statistics— per-point statistics (distance, bearing, elevation change)cumulative_distance— cumulative distance from the start of each segment/routeenhance_elevation— elevation lookup via the elevation serverfull_poi_data— POI boundary extraction (start, finish, and per-segment boundaries)
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.
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).
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.
Accessing data
result.points # => [#<Waypoint lat=…, lon=…, ele=…>, …] (all geographic points)
result.waypoints # => [#<Waypoint …>] (top-level <wpt> elements only)
result.routes # => [#<Route …>]
result.tracks # => [#<Track …>]
result. # => #<Metadata …> (or nil)
result.pois # => Hash (only when parsed with full_poi_data: true, otherwise nil)
result.points is a flat array containing all geographic points from:
- Top-level
<wpt>elements <rtept>elements inside each<rte><trkpt>elements inside each<trkseg>inside each<trk>
result.pois
When parsed with full_poi_data: true, result.pois contains boundary data for the entire GPX and each track segment:
result = GpxDoctor::Parser.parse("route.gpx", params: { full_poi_data: true })
result.pois
# =>
# {
# start: { lon: 16.38, lat: 48.23, ele: 170.0, distance: 0.0 },
# finish: { lon: 16.39, lat: 48.24, ele: 175.0, distance: 1.42 },
# segments: [
# {
# start: { lon: 16.38, lat: 48.23, ele: 170.0, distance: 0.0 },
# finish: { lon: 16.39, lat: 48.24, ele: 175.0, distance: 1.42 }
# }
# ]
# }
start/finish— first and last geographic point across all routes and track segments, each withlon,lat,distance(andelewhen present).startalways hasdistance: 0.0;finishdistance is the sum of all individual route/segment lengths.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 (startis alwaysdistance: 0.0).- The
elekey is omitted for points that have no elevation value. - Distance values respect the configured
unit_system(kilometres for:metric, miles for:imperial).
Building GPX files
The GpxDoctor::Builder class generates GPX 1.1 XML from a Result object (the same structure returned by the parser).
Build to a string
result = GpxDoctor::Parser::Result.new(
waypoints: [],
routes: [],
tracks: [],
metadata: nil
)
xml_string = GpxDoctor::Builder.build(result)
# => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<gpx version=\"1.1\"..."
# Optional: specify a custom creator attribute (identifies the software that created the GPX file)
xml_string = GpxDoctor::Builder.build(result, creator: 'My Application')
Build to a file
GpxDoctor::Builder.build_file(result, 'path/to/output.gpx')
# Writes the GPX XML to the file and returns the XML string
# Optional: specify a custom creator attribute
GpxDoctor::Builder.build_file(result, 'path/to/output.gpx', creator: 'My Application')
Input structure
The builder expects a GpxDoctor::Parser::Result object with the following fields:
waypoints— Array ofGpxDoctor::Waypointobjects (top-level waypoints)routes— Array ofGpxDoctor::Routeobjectstracks— Array ofGpxDoctor::Trackobjectsmetadata—GpxDoctor::Metadataobject (optional)
All model classes are simple Ruby objects with attributes matching the GPX 1.1 specification (see Model field reference below).
Example: Creating a GPX file from scratch
require 'gpx_doctor'
# Create waypoints
waypoint = GpxDoctor::Waypoint.new(
lat: 48.2093723,
lon: 16.356099,
ele: 160.0,
name: 'Vienna',
desc: 'Capital of Austria'
)
# Create a route with points
route_point_1 = GpxDoctor::Waypoint.new(lat: 48.21, lon: 16.36, ele: 155.0)
route_point_2 = GpxDoctor::Waypoint.new(lat: 48.22, lon: 16.37, ele: 162.0)
route = GpxDoctor::Route.new(
name: 'City Tour',
desc: 'A route through the city',
points: [route_point_1, route_point_2]
)
# Create a track with segments
track_point_1 = GpxDoctor::Waypoint.new(lat: 48.23, lon: 16.38, ele: 170.0)
track_point_2 = GpxDoctor::Waypoint.new(lat: 48.24, lon: 16.39, ele: 175.0)
segment = GpxDoctor::TrackSegment.new(points: [track_point_1, track_point_2])
track = GpxDoctor::Track.new(
name: 'Morning Run',
desc: 'My morning jog',
segments: [segment]
)
# Create metadata (optional)
= GpxDoctor::Metadata.new(
name: 'My GPX File',
desc: 'A custom GPX file',
time: Time.now
)
# Build the result object
result = GpxDoctor::Parser::Result.new(
waypoints: [waypoint],
routes: [route],
tracks: [track],
metadata:
)
# Generate GPX XML
xml_string = GpxDoctor::Builder.build(result, creator: 'My Application')
# Or write directly to a file
GpxDoctor::Builder.build_file(result, 'my_route.gpx', creator: 'My Application')
Round-trip workflow
You can parse an existing GPX file, modify it, and build it back:
# Parse existing file
result = GpxDoctor::Parser.parse('input.gpx')
# Modify data
result.routes.first.name = 'Updated Route Name'
result.waypoints << GpxDoctor::Waypoint.new(lat: 48.5, lon: 16.5, name: 'New Point')
# Build back to GPX
GpxDoctor::Builder.build_file(result, 'output.gpx')
Model field reference
Waypoint
| Field | Type | Notes |
|---|---|---|
lat |
Float | Required |
lon |
Float | Required |
ele |
Float | Elevation in metres |
time |
Time | |
magvar |
Float | Magnetic variation |
geoidheight |
Float | |
name |
String | |
cmt |
String | Comment |
desc |
String | Description |
src |
String | Source |
links |
Array | |
sym |
String | Symbol |
type |
String | |
fix |
String | none, 2d, 3d, dgps, pps |
sat |
Integer | Number of satellites |
hdop |
Float | |
vdop |
Float | |
pdop |
Float | |
ageofdgpsdata |
Float | |
dgpsid |
Integer | 0–1023 |
distance_to_next |
Float | Distance to next point (metres or feet based on unit_system) — set by segment_statistics: true |
elevation_change |
Float | Elevation change to next point (metres or feet based on unit_system) — set by segment_statistics: true |
direction |
Float | Bearing to next point (0–360°) — set by segment_statistics: true |
cumulative_distance |
Float | Cumulative distance from segment/route start (kilometres or miles based on unit_system) — set by cumulative_distance: true |
Waypoint#to_h returns a hash of all non-nil fields.
Metadata
| Field | Type |
|---|---|
name |
String |
desc |
String |
author |
Person |
copyright |
Copyright |
links |
Array |
time |
Time |
keywords |
String |
bounds |
Bounds |
Route
| Field | Type |
|---|---|
name |
String |
cmt |
String |
desc |
String |
src |
String |
links |
Array |
number |
Integer |
type |
String |
points |
Array |
Track
| Field | Type |
|---|---|
name |
String |
cmt |
String |
desc |
String |
src |
String |
links |
Array |
number |
Integer |
type |
String |
segments |
Array |
points |
Array |
TrackSegment
| Field | Type |
|---|---|
points |
Array |
Person
| Field | Type |
|---|---|
name |
String |
email |
|
link |
Link |
Copyright
| Field | Type |
|---|---|
author |
String |
year |
String |
license |
String |
Link
| Field | Type |
|---|---|
href |
String |
text |
String |
type |
String |
Email
| Field | Type |
|---|---|
id |
String |
domain |
String |
Email#to_s returns "id@domain".
Bounds
| Field | Type |
|---|---|
minlat |
Float |
minlon |
Float |
maxlat |
Float |
maxlon |
Float |
Development
Building the gem
gem build gpx_doctor.gemspec
This produces a file like gpx_doctor-0.1.0.gem in the current directory.
Running tests
bundle install
bundle exec rspec
Publishing to RubyGems
-
Create an account at https://rubygems.org if you don't have one.
-
Set up credentials (one-time):
gem signinThis stores your API key in
~/.gem/credentials. -
Build and push:
gem build gpx_doctor.gemspec gem push gpx_doctor-0.1.0.gem -
Verify the release at
https://rubygems.org/gems/gpx_doctor.
Tip: Bump
GpxDoctor::VERSIONinlib/gpx_doctor/version.rbbefore each release and tag the commit:git tag -a v0.1.0 -m "Release 0.1.0" git push origin v0.1.0
License
MIT