GPS Exchange Format library/CLI in OCaml
at main 20 kB view raw
1(** OCaml library for reading and writing GPX (GPS Exchange Format) files. 2 3 {1 Overview} 4 5 GPX (GPS Exchange Format) is an XML-based format for GPS data interchange, 6 standardized by {{:https://www.topografix.com/gpx.asp}Topografix}. This library 7 provides a complete implementation of the GPX 1.1 specification with strong 8 type safety and validation. 9 10 GPX files can contain three main types of GPS data: 11 - {b Waypoints}: Individual points of interest with coordinates 12 - {b Routes}: Ordered sequences of waypoints representing a planned path 13 - {b Tracks}: Recorded GPS traces, typically from actual journeys 14 15 All coordinates in GPX use the WGS84 datum (World Geodetic System 1984), 16 the same coordinate system used by GPS satellites. Coordinates are expressed 17 as decimal degrees, with elevations in meters above mean sea level. 18 19 {2 Quick Start Example} 20 21 {[ 22 open Gpx 23 24 (* Create coordinates *) 25 let lat = Coordinate.latitude 37.7749 |> Result.get_ok in 26 let lon = Coordinate.longitude (-122.4194) |> Result.get_ok in 27 28 (* Create a waypoint *) 29 let waypoint = Waypoint.make lat lon 30 |> Waypoint.with_name "San Francisco" 31 |> Waypoint.with_description "Golden Gate Bridge area" in 32 33 (* Create GPX document *) 34 let gpx = make_gpx ~creator:"my-app" 35 |> Doc.add_waypoint waypoint in 36 37 (* Write to file or string *) 38 match write_string gpx with 39 | Ok xml -> print_endline xml 40 | Error e -> Printf.eprintf "Error: %s\n" (Error.to_string e) 41 ]} 42 43 This library provides a clean, modular interface for working with GPX files, 44 with separate modules for each major component of the GPX specification. *) 45 46(** {1 Core Modules} 47 48 The library is organized into focused modules, each handling a specific aspect 49 of GPX data. Each module provides complete functionality for its domain with 50 strong type safety and validation. *) 51 52(** {2 Geographic coordinate handling with validation} 53 54 The {!Coordinate} module provides validated coordinate types for latitude, 55 longitude, and degrees. All coordinates use the WGS84 datum and are validated 56 at construction time to ensure they fall within valid ranges: 57 - Latitude: -90.0 to +90.0 degrees 58 - Longitude: -180.0 to +180.0 degrees 59 - Degrees: 0.0 to 360.0 degrees 60 61 Example: [Coordinate.latitude 37.7749] creates a validated latitude. *) 62module Coordinate = Coordinate 63 64(** {2 Links, persons, and copyright information} 65 66 The {!Link} module handles web links, author information, and copyright data 67 as defined in the GPX specification. This includes: 68 - Web links with optional text and MIME type 69 - Person records with name, email, and associated links 70 - Copyright information with author, year, and license terms *) 71module Link = Link 72 73(** {2 Extension mechanism for custom GPX elements} 74 75 The {!Extension} module provides support for custom XML elements that extend 76 the standard GPX format. Extensions allow applications to embed additional 77 data while maintaining compatibility with standard GPX readers. *) 78module Extension = Extension 79 80(** {2 GPS waypoint data and fix types} 81 82 The {!Waypoint} module handles individual GPS points, including waypoints, 83 route points, and track points. Each waypoint contains: 84 - Required coordinates (latitude/longitude) 85 - Optional elevation in meters above mean sea level 86 - Optional timestamp 87 - Optional metadata like name, description, symbol 88 - Optional GPS quality information (accuracy, satellite count, etc.) 89 90 Fix types indicate GPS quality: none, 2D, 3D, DGPS, or PPS. *) 91module Waypoint = Waypoint 92 93(** {2 GPX metadata including bounds} 94 95 The {!Metadata} module handles document-level information: 96 - File name and description 97 - Author and copyright information 98 - Creation time and keywords 99 - Geographic bounding box of all data 100 - Links to related resources *) 101module Metadata = Metadata 102 103(** {2 Route data and calculations} 104 105 The {!Route} module handles planned paths represented as ordered sequences 106 of waypoints. Routes typically represent intended journeys rather than 107 recorded tracks. Each route can include: 108 - Ordered list of waypoints (route points) 109 - Route metadata (name, description, links) 110 - Distance calculations between points *) 111module Route = Route 112 113(** {2 Track data with segments} 114 115 The {!Track} module handles recorded GPS traces, typically representing 116 actual journeys. Tracks are divided into segments to handle GPS interruptions: 117 - Track segments contain ordered track points 118 - Each track point is a timestamped waypoint 119 - Multiple segments per track handle GPS signal loss 120 - Distance and time calculations available *) 121module Track = Track 122 123(** {2 Error handling} 124 125 The {!Error} module provides comprehensive error handling for GPX operations: 126 - XML parsing and validation errors 127 - Coordinate validation errors 128 - Missing required elements or attributes 129 - File I/O errors *) 130module Error = Error 131 132(** {2 Main GPX document type} 133 134 The {!Doc} module represents complete GPX documents containing: 135 - Document metadata (creator, version) 136 - Collections of waypoints, routes, and tracks 137 - Document-level extensions 138 - Statistics and analysis functions *) 139module Doc = Doc 140 141(** {1 Main Document Type} *) 142 143(** A complete GPX document containing waypoints, routes, tracks, and metadata. 144 145 This is the main type representing a complete GPX file. GPX documents must 146 have a creator string (identifying the creating application) and follow the 147 GPX 1.1 specification format. *) 148type t = Doc.t 149 150(** {1 Error Handling} *) 151 152(** Comprehensive error type covering all possible GPX operation failures. 153 154 Errors can occur during: 155 - XML parsing (malformed XML, invalid structure) 156 - Coordinate validation (out of range values) 157 - Missing required GPX elements or attributes 158 - File I/O operations *) 159type error = Error.t 160 161(** GPX exception raised for unrecoverable errors. 162 163 Most functions return [Result.t] for error handling, but this exception 164 may be raised in exceptional circumstances. *) 165exception Gpx_error of error 166 167(** {1 Parsing Functions} 168 169 Parse GPX data from various sources. All parsing functions support optional 170 validation to check compliance with GPX specification constraints. *) 171 172(** Parse GPX from XML input source. 173 174 Reads GPX data from an {!Xmlm.input} source, which can be created from 175 files, strings, or other input sources using the {{:https://erratique.ch/software/xmlm}Xmlm} library. 176 177 @param validate If [true] (default [false]), validates the parsed document 178 against GPX specification rules. Validation checks coordinate 179 ranges, required elements, and data consistency. 180 @param input XMLm input source created with [Xmlm.make_input] 181 @return [Ok gpx] with parsed document, or [Error e] if parsing fails 182 183 Example: 184 {[ 185 let input = Xmlm.make_input (`String (0, gpx_xml_string)) in 186 match parse ~validate:true input with 187 | Ok gpx -> Printf.printf "Parsed %d waypoints\n" (List.length (Doc.waypoints gpx)) 188 | Error e -> Printf.eprintf "Parse error: %s\n" (Error.to_string e) 189 ]} *) 190val parse : ?validate:bool -> Xmlm.input -> (t, error) result 191 192(** Parse GPX from XML string. 193 194 Convenience function for parsing GPX data from a string. Equivalent to 195 creating an {!Xmlm.input} from the string and calling {!parse}. 196 197 @param validate If [true] (default [false]), validates the parsed document 198 @param s Complete GPX XML document as a string 199 @return [Ok gpx] with parsed document, or [Error e] if parsing fails 200 201 Example: 202 {[ 203 let gpx_xml = {|<?xml version="1.0"?> 204 <gpx version="1.1" creator="my-app"> 205 <wpt lat="37.7749" lon="-122.4194"> 206 <name>San Francisco</name> 207 </wpt> 208 </gpx>|} in 209 match parse_string ~validate:true gpx_xml with 210 | Ok gpx -> print_endline "Successfully parsed GPX" 211 | Error e -> Printf.eprintf "Error: %s\n" (Error.to_string e) 212 ]} *) 213val parse_string : ?validate:bool -> string -> (t, error) result 214 215(** {1 Writing Functions} 216 217 Generate GPX XML from document structures. All writing functions support 218 optional validation before output generation. *) 219 220(** Write GPX to XML output destination. 221 222 Generates standard GPX 1.1 XML and writes it to an {!Xmlm.dest} destination. 223 The output destination can target files, buffers, or other sinks. 224 225 @param validate If [true] (default [false]), validates the document before 226 writing to ensure GPX specification compliance 227 @param dest XMLm output destination created with [Xmlm.make_output] 228 @param gpx GPX document to write 229 @return [Ok ()] on success, or [Error e] if writing fails 230 231 Example: 232 {[ 233 let output = Buffer.create 1024 in 234 let dest = Xmlm.make_output (`Buffer output) in 235 match write ~validate:true dest gpx with 236 | Ok () -> Buffer.contents output 237 | Error e -> failwith (Error.to_string e) 238 ]} *) 239val write : ?validate:bool -> Xmlm.dest -> t -> (unit, error) result 240 241(** Write GPX to XML string. 242 243 Convenience function that generates a complete GPX XML document as a string. 244 The output includes XML declaration and proper namespace declarations. 245 246 @param validate If [true] (default [false]), validates before writing 247 @param gpx GPX document to serialize 248 @return [Ok xml_string] with complete GPX XML, or [Error e] if generation fails 249 250 Example: 251 {[ 252 match write_string ~validate:true gpx with 253 | Ok xml -> 254 print_endline "Generated GPX:"; 255 print_endline xml 256 | Error e -> 257 Printf.eprintf "Failed to generate GPX: %s\n" (Error.to_string e) 258 ]} *) 259val write_string : ?validate:bool -> t -> (string, error) result 260 261(** {1 Validation Functions} 262 263 Comprehensive validation against GPX specification rules and best practices. 264 Validation checks coordinate ranges, required elements, data consistency, 265 and common issues that may cause problems for GPS applications. *) 266 267(** A validation issue found during GPX document checking. 268 269 Issues are classified as either errors (specification violations that make 270 the GPX invalid) or warnings (best practice violations or suspicious data). *) 271type validation_issue = Validate.validation_issue = { 272 level : [`Error | `Warning]; (** [`Error] for specification violations, [`Warning] for best practice issues *) 273 message : string; (** Human-readable description of the issue *) 274 location : string option; (** Optional location context (e.g., "waypoint 1", "track segment 2") *) 275} 276 277(** Complete validation result with all issues and validity status. 278 279 The [is_valid] field indicates whether the document contains any errors. 280 Documents with only warnings are considered valid. *) 281type validation_result = Validate.validation_result = { 282 issues : validation_issue list; (** All validation issues found, both errors and warnings *) 283 is_valid : bool; (** [true] if no errors found (warnings are allowed) *) 284} 285 286(** Perform comprehensive validation of a GPX document. 287 288 Checks all aspects of the GPX document against the specification: 289 - Coordinate ranges (latitude -90 to +90, longitude -180 to +180) 290 - Required elements and attributes 291 - Data consistency (e.g., time ordering in tracks) 292 - Reasonable value ranges for GPS quality metrics 293 - Proper structure and nesting 294 295 @param gpx The GPX document to validate 296 @return Complete validation result with all issues found 297 298 Example: 299 {[ 300 let result = validate_gpx gpx in 301 if result.is_valid then 302 Printf.printf "Document is valid with %d warnings\n" 303 (List.length (List.filter (fun i -> i.level = `Warning) result.issues)) 304 else begin 305 print_endline "Document has errors:"; 306 List.iter (fun issue -> 307 if issue.level = `Error then 308 Printf.printf " ERROR: %s\n" (format_issue issue) 309 ) result.issues 310 end 311 ]} *) 312val validate_gpx : t -> validation_result 313 314(** Quick validation check - returns true if document has no errors. 315 316 Equivalent to [(validate_gpx gpx).is_valid] but potentially more efficient 317 as it can stop at the first error found. 318 319 @param gpx The GPX document to validate 320 @return [true] if valid (no errors), [false] if errors found *) 321val is_valid : t -> bool 322 323(** Get only validation errors (specification violations). 324 325 Returns only the issues marked as errors, filtering out warnings. 326 If this list is empty, the document is valid according to the GPX specification. 327 328 @param gpx The GPX document to validate 329 @return List of error-level validation issues *) 330val errors : t -> validation_issue list 331 332(** Get only validation warnings (best practice violations). 333 334 Returns only the issues marked as warnings. These don't make the document 335 invalid but may indicate potential problems or areas for improvement. 336 337 @param gpx The GPX document to validate 338 @return List of warning-level validation issues *) 339val warnings : t -> validation_issue list 340 341(** Format a validation issue for human-readable display. 342 343 Combines the issue message with location context if available. 344 345 @param issue The validation issue to format 346 @return Formatted string suitable for display to users 347 348 Example output: ["Error in waypoint 1: Latitude out of range (-95.0)"] *) 349val format_issue : validation_issue -> string 350 351(** {1 Document Constructors and Utilities} 352 353 Functions for creating GPX documents and basic document operations. *) 354 355(** Create a new GPX document with the required creator field. 356 357 Every GPX document must identify its creating application through the 358 [creator] attribute. This is required by the GPX specification and helps 359 identify the source of GPS data. 360 361 The created document: 362 - Uses GPX version 1.1 (the current standard) 363 - Contains no waypoints, routes, or tracks initially 364 - Has no metadata initially 365 - Can be extended using {!Doc} module functions 366 367 @param creator Name of the creating application (e.g., "MyGPS App v1.0") 368 @return Empty GPX document ready for data addition 369 370 Example: 371 {[ 372 let gpx = make_gpx ~creator:"MyTracker v2.1" in 373 let gpx = Doc.add_waypoint gpx some_waypoint in 374 let gpx = Doc.add_track gpx some_track in 375 (* gpx now contains waypoints and tracks *) 376 ]} *) 377val make_gpx : creator:string -> t 378 379(** Create an empty GPX document with the required creator field. 380 381 Alias for {!make_gpx} provided for consistency with module naming patterns. 382 Creates a document with no GPS data that can be populated using the 383 {!Doc} module functions. 384 385 @param creator Name of the creating application 386 @return Empty GPX document 387 388 Example: 389 {[ 390 let gpx = empty ~creator:"GPS Logger" in 391 assert (List.length (Doc.waypoints gpx) = 0); 392 assert (List.length (Doc.tracks gpx) = 0); 393 assert (Doc.creator gpx = "GPS Logger"); 394 ]} *) 395val empty : creator:string -> t 396 397(** {1 Common Patterns and Best Practices} 398 399 {2 Reading GPX Files} 400 401 The most common use case is reading existing GPX files: 402 {[ 403 (* From a file using platform-specific modules *) 404 match Gpx_unix.read "track.gpx" with 405 | Ok gpx -> process_gpx gpx 406 | Error e -> handle_error e 407 408 (* From a string *) 409 match parse_string ~validate:true gpx_content with 410 | Ok gpx -> process_gpx gpx 411 | Error e -> handle_error e 412 ]} 413 414 {2 Creating GPX Files} 415 416 To create new GPX files with waypoints: 417 {[ 418 (* Create coordinates *) 419 let lat = Coordinate.latitude 37.7749 |> Result.get_ok in 420 let lon = Coordinate.longitude (-122.4194) |> Result.get_ok in 421 422 (* Create waypoint *) 423 let waypoint = Waypoint.make lat lon 424 |> Waypoint.with_name "Golden Gate" 425 |> Waypoint.with_description "Famous San Francisco bridge" in 426 427 (* Create document *) 428 let gpx = make_gpx ~creator:"My App v1.0" 429 |> Doc.add_waypoint waypoint in 430 431 (* Write to file *) 432 match Gpx_unix.write "output.gpx" gpx with 433 | Ok () -> print_endline "File written successfully" 434 | Error e -> Printf.eprintf "Write error: %s\n" (Error.to_string e) 435 ]} 436 437 {2 Working with Tracks} 438 439 Tracks represent recorded GPS traces with timestamped points: 440 {[ 441 (* Create track points with timestamps *) 442 let points = List.map (fun (lat_f, lon_f, time) -> 443 let lat = Coordinate.latitude lat_f |> Result.get_ok in 444 let lon = Coordinate.longitude lon_f |> Result.get_ok in 445 Waypoint.make lat lon |> Waypoint.with_time (Some time) 446 ) gps_data in 447 448 (* Create track segment *) 449 let segment = Track.Segment.make points in 450 451 (* Create track *) 452 let track = Track.make ~name:"Morning Run" 453 |> Track.add_segment segment in 454 455 (* Add to document *) 456 let gpx = make_gpx ~creator:"Fitness App" 457 |> Doc.add_track track 458 ]} 459 460 {2 Coordinate Systems and Units} 461 462 - All coordinates use WGS84 datum (World Geodetic System 1984) 463 - Latitude ranges from -90.0 (South Pole) to +90.0 (North Pole) 464 - Longitude ranges from -180.0 to +180.0 degrees 465 - Elevations are in meters above mean sea level 466 - Times use RFC 3339 format (ISO 8601 subset) 467 468 {2 Validation Recommendations} 469 470 - Always validate when parsing untrusted GPX data 471 - Validate before writing to catch data consistency issues 472 - Handle both errors and warnings appropriately 473 - Use {!val:is_valid} for quick checks, {!validate_gpx} for detailed analysis 474 475 {2 Performance Considerations} 476 477 - The library uses streaming XML parsing for memory efficiency 478 - Large GPX files with many track points are handled efficiently 479 - Coordinate validation occurs at construction time 480 - Consider using platform-specific modules ({!Gpx_unix}, [Gpx_eio]) for file I/O 481 482 {2 Extension Support} 483 484 The library supports GPX extensions for custom data: 485 {[ 486 (* Create extension *) 487 let ext = Extension.make_text 488 ~name:"temperature" 489 ~namespace:"http://example.com/weather" 490 "25.5" in 491 492 (* Add to waypoint *) 493 let waypoint = Waypoint.make lat lon 494 |> Waypoint.add_extensions [ext] 495 ]} *) 496 497(** {1 Related Modules and Libraries} 498 499 This core module provides the foundation. For complete applications, consider: 500 501 - {!Gpx_unix}: File I/O operations using standard Unix libraries 502 - {!Gpx_eio}: Concurrent file I/O using the Eio effects library 503 - {{:https://erratique.ch/software/xmlm}Xmlm}: Underlying XML processing library 504 - {{:https://erratique.ch/software/ptime}Ptime}: Time representation used for timestamps 505 506 {2 External Links} 507 508 - {{:https://www.topografix.com/gpx.asp}Official GPX specification} 509 - {{:https://www.topografix.com/GPX/1/1/gpx.xsd}GPX 1.1 XML Schema} 510 - {{:https://en.wikipedia.org/wiki/GPS_Exchange_Format}GPX Format on Wikipedia} 511 - {{:https://en.wikipedia.org/wiki/World_Geodetic_System}WGS84 Coordinate System} *)