···
+
(** OCaml library for reading and writing GPX (GPS Exchange Format) files.
+
GPX (GPS Exchange Format) is an XML-based format for GPS data interchange,
+
standardized by {{:https://www.topografix.com/gpx.asp}Topografix}. This library
+
provides a complete implementation of the GPX 1.1 specification with strong
+
type safety and validation.
+
GPX files can contain three main types of GPS data:
+
- {b Waypoints}: Individual points of interest with coordinates
+
- {b Routes}: Ordered sequences of waypoints representing a planned path
+
- {b Tracks}: Recorded GPS traces, typically from actual journeys
+
All coordinates in GPX use the WGS84 datum (World Geodetic System 1984),
+
the same coordinate system used by GPS satellites. Coordinates are expressed
+
as decimal degrees, with elevations in meters above mean sea level.
+
{2 Quick Start Example}
+
(* Create coordinates *)
+
let lat = Coordinate.latitude 37.7749 |> Result.get_ok in
+
let lon = Coordinate.longitude (-122.4194) |> Result.get_ok in
+
(* Create a waypoint *)
+
let waypoint = Waypoint.make lat lon
+
|> Waypoint.with_name "San Francisco"
+
|> Waypoint.with_description "Golden Gate Bridge area" in
+
(* Create GPX document *)
+
let gpx = make_gpx ~creator:"my-app"
+
|> Doc.add_waypoint waypoint in
+
(* Write to file or string *)
+
match write_string gpx with
+
| Ok xml -> print_endline xml
+
| Error e -> Printf.eprintf "Error: %s\n" (Error.to_string e)
This library provides a clean, modular interface for working with GPX files,
+
with separate modules for each major component of the GPX specification. *)
The library is organized into focused modules, each handling a specific aspect
+
of GPX data. Each module provides complete functionality for its domain with
+
strong type safety and validation. *)
+
(** {2 Geographic coordinate handling with validation}
+
The {!Coordinate} module provides validated coordinate types for latitude,
+
longitude, and degrees. All coordinates use the WGS84 datum and are validated
+
at construction time to ensure they fall within valid ranges:
+
- Latitude: -90.0 to +90.0 degrees
+
- Longitude: -180.0 to +180.0 degrees
+
- Degrees: 0.0 to 360.0 degrees
+
Example: [Coordinate.latitude 37.7749] creates a validated latitude. *)
module Coordinate = Coordinate
+
(** {2 Links, persons, and copyright information}
+
The {!Link} module handles web links, author information, and copyright data
+
as defined in the GPX specification. This includes:
+
- Web links with optional text and MIME type
+
- Person records with name, email, and associated links
+
- Copyright information with author, year, and license terms *)
+
(** {2 Extension mechanism for custom GPX elements}
+
The {!Extension} module provides support for custom XML elements that extend
+
the standard GPX format. Extensions allow applications to embed additional
+
data while maintaining compatibility with standard GPX readers. *)
module Extension = Extension
+
(** {2 GPS waypoint data and fix types}
+
The {!Waypoint} module handles individual GPS points, including waypoints,
+
route points, and track points. Each waypoint contains:
+
- Required coordinates (latitude/longitude)
+
- Optional elevation in meters above mean sea level
+
- Optional metadata like name, description, symbol
+
- Optional GPS quality information (accuracy, satellite count, etc.)
+
Fix types indicate GPS quality: none, 2D, 3D, DGPS, or PPS. *)
module Waypoint = Waypoint
+
(** {2 GPX metadata including bounds}
+
The {!Metadata} module handles document-level information:
+
- File name and description
+
- Author and copyright information
+
- Creation time and keywords
+
- Geographic bounding box of all data
+
- Links to related resources *)
module Metadata = Metadata
+
(** {2 Route data and calculations}
+
The {!Route} module handles planned paths represented as ordered sequences
+
of waypoints. Routes typically represent intended journeys rather than
+
recorded tracks. Each route can include:
+
- Ordered list of waypoints (route points)
+
- Route metadata (name, description, links)
+
- Distance calculations between points *)
+
(** {2 Track data with segments}
+
The {!Track} module handles recorded GPS traces, typically representing
+
actual journeys. Tracks are divided into segments to handle GPS interruptions:
+
- Track segments contain ordered track points
+
- Each track point is a timestamped waypoint
+
- Multiple segments per track handle GPS signal loss
+
- Distance and time calculations available *)
+
The {!Error} module provides comprehensive error handling for GPX operations:
+
- XML parsing and validation errors
+
- Coordinate validation errors
+
- Missing required elements or attributes
+
(** {2 Main GPX document type}
+
The {!Doc} module represents complete GPX documents containing:
+
- Document metadata (creator, version)
+
- Collections of waypoints, routes, and tracks
+
- Document-level extensions
+
- Statistics and analysis functions *)
(** {1 Main Document Type} *)
+
(** A complete GPX document containing waypoints, routes, tracks, and metadata.
+
This is the main type representing a complete GPX file. GPX documents must
+
have a creator string (identifying the creating application) and follow the
+
GPX 1.1 specification format. *)
(** {1 Error Handling} *)
+
(** Comprehensive error type covering all possible GPX operation failures.
+
Errors can occur during:
+
- XML parsing (malformed XML, invalid structure)
+
- Coordinate validation (out of range values)
+
- Missing required GPX elements or attributes
+
- File I/O operations *)
+
(** GPX exception raised for unrecoverable errors.
+
Most functions return [Result.t] for error handling, but this exception
+
may be raised in exceptional circumstances. *)
exception Gpx_error of error
+
(** {1 Parsing Functions}
+
Parse GPX data from various sources. All parsing functions support optional
+
validation to check compliance with GPX specification constraints. *)
+
(** Parse GPX from XML input source.
+
Reads GPX data from an {!Xmlm.input} source, which can be created from
+
files, strings, or other input sources using the {{:https://erratique.ch/software/xmlm}Xmlm} library.
+
@param validate If [true] (default [false]), validates the parsed document
+
against GPX specification rules. Validation checks coordinate
+
ranges, required elements, and data consistency.
+
@param input XMLm input source created with [Xmlm.make_input]
+
@return [Ok gpx] with parsed document, or [Error e] if parsing fails
+
let input = Xmlm.make_input (`String (0, gpx_xml_string)) in
+
match parse ~validate:true input with
+
| Ok gpx -> Printf.printf "Parsed %d waypoints\n" (List.length (Doc.waypoints gpx))
+
| Error e -> Printf.eprintf "Parse error: %s\n" (Error.to_string e)
val parse : ?validate:bool -> Xmlm.input -> (t, error) result
+
(** Parse GPX from XML string.
+
Convenience function for parsing GPX data from a string. Equivalent to
+
creating an {!Xmlm.input} from the string and calling {!parse}.
+
@param validate If [true] (default [false]), validates the parsed document
+
@param s Complete GPX XML document as a string
+
@return [Ok gpx] with parsed document, or [Error e] if parsing fails
+
let gpx_xml = {|<?xml version="1.0"?>
+
<gpx version="1.1" creator="my-app">
+
<wpt lat="37.7749" lon="-122.4194">
+
<name>San Francisco</name>
+
match parse_string ~validate:true gpx_xml with
+
| Ok gpx -> print_endline "Successfully parsed GPX"
+
| Error e -> Printf.eprintf "Error: %s\n" (Error.to_string e)
val parse_string : ?validate:bool -> string -> (t, error) result
+
(** {1 Writing Functions}
+
Generate GPX XML from document structures. All writing functions support
+
optional validation before output generation. *)
+
(** Write GPX to XML output destination.
+
Generates standard GPX 1.1 XML and writes it to an {!Xmlm.dest} destination.
+
The output destination can target files, buffers, or other sinks.
+
@param validate If [true] (default [false]), validates the document before
+
writing to ensure GPX specification compliance
+
@param dest XMLm output destination created with [Xmlm.make_output]
@param gpx GPX document to write
+
@return [Ok ()] on success, or [Error e] if writing fails
+
let output = Buffer.create 1024 in
+
let dest = Xmlm.make_output (`Buffer output) in
+
match write ~validate:true dest gpx with
+
| Ok () -> Buffer.contents output
+
| Error e -> failwith (Error.to_string e)
val write : ?validate:bool -> Xmlm.dest -> t -> (unit, error) result
+
(** Write GPX to XML string.
+
Convenience function that generates a complete GPX XML document as a string.
+
The output includes XML declaration and proper namespace declarations.
+
@param validate If [true] (default [false]), validates before writing
+
@param gpx GPX document to serialize
+
@return [Ok xml_string] with complete GPX XML, or [Error e] if generation fails
+
match write_string ~validate:true gpx with
+
print_endline "Generated GPX:";
+
Printf.eprintf "Failed to generate GPX: %s\n" (Error.to_string e)
val write_string : ?validate:bool -> t -> (string, error) result
(** {1 Validation Functions}
+
Comprehensive validation against GPX specification rules and best practices.
+
Validation checks coordinate ranges, required elements, data consistency,
+
and common issues that may cause problems for GPS applications. *)
+
(** A validation issue found during GPX document checking.
+
Issues are classified as either errors (specification violations that make
+
the GPX invalid) or warnings (best practice violations or suspicious data). *)
type validation_issue = Validate.validation_issue = {
+
level : [`Error | `Warning]; (** [`Error] for specification violations, [`Warning] for best practice issues *)
+
message : string; (** Human-readable description of the issue *)
+
location : string option; (** Optional location context (e.g., "waypoint 1", "track segment 2") *)
+
(** Complete validation result with all issues and validity status.
+
The [is_valid] field indicates whether the document contains any errors.
+
Documents with only warnings are considered valid. *)
type validation_result = Validate.validation_result = {
+
issues : validation_issue list; (** All validation issues found, both errors and warnings *)
+
is_valid : bool; (** [true] if no errors found (warnings are allowed) *)
+
(** Perform comprehensive validation of a GPX document.
+
Checks all aspects of the GPX document against the specification:
+
- Coordinate ranges (latitude -90 to +90, longitude -180 to +180)
+
- Required elements and attributes
+
- Data consistency (e.g., time ordering in tracks)
+
- Reasonable value ranges for GPS quality metrics
+
- Proper structure and nesting
+
@param gpx The GPX document to validate
+
@return Complete validation result with all issues found
+
let result = validate_gpx gpx in
+
if result.is_valid then
+
Printf.printf "Document is valid with %d warnings\n"
+
(List.length (List.filter (fun i -> i.level = `Warning) result.issues))
+
print_endline "Document has errors:";
+
List.iter (fun issue ->
+
if issue.level = `Error then
+
Printf.printf " ERROR: %s\n" (format_issue issue)
val validate_gpx : t -> validation_result
+
(** Quick validation check - returns true if document has no errors.
+
Equivalent to [(validate_gpx gpx).is_valid] but potentially more efficient
+
as it can stop at the first error found.
+
@param gpx The GPX document to validate
+
@return [true] if valid (no errors), [false] if errors found *)
+
(** Get only validation errors (specification violations).
+
Returns only the issues marked as errors, filtering out warnings.
+
If this list is empty, the document is valid according to the GPX specification.
+
@param gpx The GPX document to validate
+
@return List of error-level validation issues *)
+
val errors : t -> validation_issue list
+
(** Get only validation warnings (best practice violations).
+
Returns only the issues marked as warnings. These don't make the document
+
invalid but may indicate potential problems or areas for improvement.
+
@param gpx The GPX document to validate
+
@return List of warning-level validation issues *)
+
val warnings : t -> validation_issue list
+
(** Format a validation issue for human-readable display.
+
Combines the issue message with location context if available.
+
@param issue The validation issue to format
+
@return Formatted string suitable for display to users
+
Example output: ["Error in waypoint 1: Latitude out of range (-95.0)"] *)
val format_issue : validation_issue -> string
+
(** {1 Document Constructors and Utilities}
+
Functions for creating GPX documents and basic document operations. *)
+
(** Create a new GPX document with the required creator field.
+
Every GPX document must identify its creating application through the
+
[creator] attribute. This is required by the GPX specification and helps
+
identify the source of GPS data.
+
- Uses GPX version 1.1 (the current standard)
+
- Contains no waypoints, routes, or tracks initially
+
- Has no metadata initially
+
- Can be extended using {!Doc} module functions
+
@param creator Name of the creating application (e.g., "MyGPS App v1.0")
+
@return Empty GPX document ready for data addition
+
let gpx = make_gpx ~creator:"MyTracker v2.1" in
+
let gpx = Doc.add_waypoint gpx some_waypoint in
+
let gpx = Doc.add_track gpx some_track in
+
(* gpx now contains waypoints and tracks *)
val make_gpx : creator:string -> t
+
(** Create an empty GPX document with the required creator field.
+
Alias for {!make_gpx} provided for consistency with module naming patterns.
+
Creates a document with no GPS data that can be populated using the
+
{!Doc} module functions.
+
@param creator Name of the creating application
+
@return Empty GPX document
+
let gpx = empty ~creator:"GPS Logger" in
+
assert (List.length (Doc.waypoints gpx) = 0);
+
assert (List.length (Doc.tracks gpx) = 0);
+
assert (Doc.creator gpx = "GPS Logger");
+
val empty : creator:string -> t
+
(** {1 Common Patterns and Best Practices}
+
The most common use case is reading existing GPX files:
+
(* From a file using platform-specific modules *)
+
match Gpx_unix.read "track.gpx" with
+
| Ok gpx -> process_gpx gpx
+
| Error e -> handle_error e
+
match parse_string ~validate:true gpx_content with
+
| Ok gpx -> process_gpx gpx
+
| Error e -> handle_error e
+
To create new GPX files with waypoints:
+
(* Create coordinates *)
+
let lat = Coordinate.latitude 37.7749 |> Result.get_ok in
+
let lon = Coordinate.longitude (-122.4194) |> Result.get_ok in
+
let waypoint = Waypoint.make lat lon
+
|> Waypoint.with_name "Golden Gate"
+
|> Waypoint.with_description "Famous San Francisco bridge" in
+
let gpx = make_gpx ~creator:"My App v1.0"
+
|> Doc.add_waypoint waypoint in
+
match Gpx_unix.write "output.gpx" gpx with
+
| Ok () -> print_endline "File written successfully"
+
| Error e -> Printf.eprintf "Write error: %s\n" (Error.to_string e)
+
{2 Working with Tracks}
+
Tracks represent recorded GPS traces with timestamped points:
+
(* Create track points with timestamps *)
+
let points = List.map (fun (lat_f, lon_f, time) ->
+
let lat = Coordinate.latitude lat_f |> Result.get_ok in
+
let lon = Coordinate.longitude lon_f |> Result.get_ok in
+
Waypoint.make lat lon |> Waypoint.with_time (Some time)
+
(* Create track segment *)
+
let segment = Track.Segment.make points in
+
let track = Track.make ~name:"Morning Run"
+
|> Track.add_segment segment in
+
let gpx = make_gpx ~creator:"Fitness App"
+
{2 Coordinate Systems and Units}
+
- All coordinates use WGS84 datum (World Geodetic System 1984)
+
- Latitude ranges from -90.0 (South Pole) to +90.0 (North Pole)
+
- Longitude ranges from -180.0 to +180.0 degrees
+
- Elevations are in meters above mean sea level
+
- Times use RFC 3339 format (ISO 8601 subset)
+
{2 Validation Recommendations}
+
- Always validate when parsing untrusted GPX data
+
- Validate before writing to catch data consistency issues
+
- Handle both errors and warnings appropriately
+
- Use {!val:is_valid} for quick checks, {!validate_gpx} for detailed analysis
+
{2 Performance Considerations}
+
- The library uses streaming XML parsing for memory efficiency
+
- Large GPX files with many track points are handled efficiently
+
- Coordinate validation occurs at construction time
+
- Consider using platform-specific modules ({!Gpx_unix}, [Gpx_eio]) for file I/O
+
The library supports GPX extensions for custom data:
+
let ext = Extension.make_text
+
~namespace:"http://example.com/weather"
+
let waypoint = Waypoint.make lat lon
+
|> Waypoint.add_extensions [ext]
+
(** {1 Related Modules and Libraries}
+
This core module provides the foundation. For complete applications, consider:
+
- {!Gpx_unix}: File I/O operations using standard Unix libraries
+
- [Gpx_eio]: Concurrent file I/O using the Eio effects library
+
- {{:https://erratique.ch/software/xmlm}Xmlm}: Underlying XML processing library
+
- {{:https://erratique.ch/software/ptime}Ptime}: Time representation used for timestamps
+
- {{:https://www.topografix.com/gpx.asp}Official GPX specification}
+
- {{:https://www.topografix.com/GPX/1/1/gpx.xsd}GPX 1.1 XML Schema}
+
- {{:https://en.wikipedia.org/wiki/GPS_Exchange_Format}GPX Format on Wikipedia}
+
- {{:https://en.wikipedia.org/wiki/World_Geodetic_System}WGS84 Coordinate System} *)