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
})
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 server
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).
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.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>
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