GPS Exchange Format library/CLI in OCaml
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} *)