(** OCaml library for reading and writing GPX (GPS Exchange Format) files. {1 Overview} 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} {[ open Gpx (* 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. *) (** {1 Core Modules} 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 *) module Link = Link (** {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 timestamp - 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 *) module Route = Route (** {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 *) module Track = Track (** {2 Error handling} The {!Error} module provides comprehensive error handling for GPX operations: - XML parsing and validation errors - Coordinate validation errors - Missing required elements or attributes - File I/O errors *) module Error = Error (** {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 *) module Doc = Doc (** {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. *) type t = Doc.t (** {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 *) type error = Error.t (** 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 Example: {[ 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 Example: {[ let gpx_xml = {| San Francisco |} in 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 Example: {[ 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 Example: {[ match write_string ~validate:true gpx with | Ok xml -> print_endline "Generated GPX:"; print_endline xml | Error e -> 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 Example: {[ 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)) else begin print_endline "Document has errors:"; List.iter (fun issue -> if issue.level = `Error then Printf.printf " ERROR: %s\n" (format_issue issue) ) result.issues end ]} *) 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 *) val is_valid : t -> bool (** 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. The created document: - 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 Example: {[ 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 Example: {[ 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} {2 Reading GPX Files} 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 (* From a string *) match parse_string ~validate:true gpx_content with | Ok gpx -> process_gpx gpx | Error e -> handle_error e ]} {2 Creating GPX Files} 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 (* Create waypoint *) let waypoint = Waypoint.make lat lon |> Waypoint.with_name "Golden Gate" |> Waypoint.with_description "Famous San Francisco bridge" in (* Create document *) let gpx = make_gpx ~creator:"My App v1.0" |> Doc.add_waypoint waypoint in (* Write to file *) 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) ) gps_data in (* Create track segment *) let segment = Track.Segment.make points in (* Create track *) let track = Track.make ~name:"Morning Run" |> Track.add_segment segment in (* Add to document *) let gpx = make_gpx ~creator:"Fitness App" |> Doc.add_track track ]} {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 {2 Extension Support} The library supports GPX extensions for custom data: {[ (* Create extension *) let ext = Extension.make_text ~name:"temperature" ~namespace:"http://example.com/weather" "25.5" in (* Add to waypoint *) 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 {2 External Links} - {{: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} *)