···
1
-
(** OCaml library for reading and writing GPX (GPS Exchange Format) files
1
+
(** OCaml library for reading and writing GPX (GPS Exchange Format) files.
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.
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
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.
19
+
{2 Quick Start Example}
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
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
33
+
(* Create GPX document *)
34
+
let gpx = make_gpx ~creator:"my-app"
35
+
|> Doc.add_waypoint waypoint in
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)
This library provides a clean, modular interface for working with GPX files,
4
-
the standard format for GPS data exchange. *)
44
+
with separate modules for each major component of the GPX specification. *)
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. *)
11
-
(** Geographic coordinate handling with validation *)
52
+
(** {2 Geographic coordinate handling with validation}
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
61
+
Example: [Coordinate.latitude 37.7749] creates a validated latitude. *)
module Coordinate = Coordinate
14
-
(** Links, persons, and copyright information *)
64
+
(** {2 Links, persons, and copyright information}
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 *)
17
-
(** Extension mechanism for custom GPX elements *)
73
+
(** {2 Extension mechanism for custom GPX elements}
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. *)
module Extension = Extension
20
-
(** GPS waypoint data and fix types *)
80
+
(** {2 GPS waypoint data and fix types}
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.)
90
+
Fix types indicate GPS quality: none, 2D, 3D, DGPS, or PPS. *)
module Waypoint = Waypoint
23
-
(** GPX metadata including bounds *)
93
+
(** {2 GPX metadata including bounds}
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 *)
module Metadata = Metadata
26
-
(** Route data and calculations *)
103
+
(** {2 Route data and calculations}
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 *)
29
-
(** Track data with segments *)
113
+
(** {2 Track data with segments}
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 *)
32
-
(** Error handling *)
123
+
(** {2 Error handling}
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 *)
35
-
(** Main GPX document type *)
36
-
module Gpx_doc = Gpx_doc
132
+
(** {2 Main GPX document type}
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 *)
(** {1 Main Document Type} *)
40
-
(** Main GPX document type *)
143
+
(** A complete GPX document containing waypoints, routes, tracks, and metadata.
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. *)
(** {1 Error Handling} *)
152
+
(** Comprehensive error type covering all possible GPX operation failures.
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 *)
48
-
(** GPX exception raised for errors *)
161
+
(** GPX exception raised for unrecoverable errors.
163
+
Most functions return [Result.t] for error handling, but this exception
164
+
may be raised in exceptional circumstances. *)
exception Gpx_error of error
51
-
(** {1 Parsing Functions} *)
167
+
(** {1 Parsing Functions}
169
+
Parse GPX data from various sources. All parsing functions support optional
170
+
validation to check compliance with GPX specification constraints. *)
53
-
(** Parse GPX from XML input.
172
+
(** Parse GPX from XML input source.
55
-
@param validate Whether to validate the document after parsing
56
-
@param input XMLm input source
57
-
@return Parsed GPX document or error *)
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.
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
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)
val parse : ?validate:bool -> Xmlm.input -> (t, error) result
60
-
(** Parse GPX from string.
192
+
(** Parse GPX from XML string.
194
+
Convenience function for parsing GPX data from a string. Equivalent to
195
+
creating an {!Xmlm.input} from the string and calling {!parse}.
62
-
@param validate Whether to validate the document after parsing
63
-
@param s XML string to parse
64
-
@return Parsed GPX document or error *)
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
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>
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)
val parse_string : ?validate:bool -> string -> (t, error) result
67
-
(** {1 Writing Functions} *)
215
+
(** {1 Writing Functions}
217
+
Generate GPX XML from document structures. All writing functions support
218
+
optional validation before output generation. *)
69
-
(** Write GPX to XML output.
220
+
(** Write GPX to XML output destination.
71
-
@param validate Whether to validate before writing
72
-
@param output XMLm output destination
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.
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]
@param gpx GPX document to write
74
-
@return Success or error *)
229
+
@return [Ok ()] on success, or [Error e] if writing fails
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)
val write : ?validate:bool -> Xmlm.dest -> t -> (unit, error) result
77
-
(** Write GPX to string.
241
+
(** Write GPX to XML string.
243
+
Convenience function that generates a complete GPX XML document as a string.
244
+
The output includes XML declaration and proper namespace declarations.
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
79
-
@param validate Whether to validate before writing
80
-
@param gpx GPX document to write
81
-
@return XML string or error *)
252
+
match write_string ~validate:true gpx with
254
+
print_endline "Generated GPX:";
257
+
Printf.eprintf "Failed to generate GPX: %s\n" (Error.to_string e)
val write_string : ?validate:bool -> t -> (string, error) result
(** {1 Validation Functions}
86
-
Validate GPX documents for correctness and best practices. *)
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. *)
88
-
(** Validation issue with severity level *)
267
+
(** A validation issue found during GPX document checking.
269
+
Issues are classified as either errors (specification violations that make
270
+
the GPX invalid) or warnings (best practice violations or suspicious data). *)
type validation_issue = Validate.validation_issue = {
90
-
level : [`Error | `Warning]; (** Severity level *)
91
-
message : string; (** Issue description *)
92
-
location : string option; (** Location in document *)
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") *)
95
-
(** Result of validation containing all issues found *)
277
+
(** Complete validation result with all issues and validity status.
279
+
The [is_valid] field indicates whether the document contains any errors.
280
+
Documents with only warnings are considered valid. *)
type validation_result = Validate.validation_result = {
97
-
issues : validation_issue list; (** All validation issues *)
98
-
is_valid : bool; (** Whether document is valid *)
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) *)
101
-
(** Validate complete GPX document *)
286
+
(** Perform comprehensive validation of a GPX document.
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
295
+
@param gpx The GPX document to validate
296
+
@return Complete validation result with all issues found
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))
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)
val validate_gpx : t -> validation_result
104
-
(** Quick validation - returns true if document is valid *)
314
+
(** Quick validation check - returns true if document has no errors.
316
+
Equivalent to [(validate_gpx gpx).is_valid] but potentially more efficient
317
+
as it can stop at the first error found.
319
+
@param gpx The GPX document to validate
320
+
@return [true] if valid (no errors), [false] if errors found *)
107
-
(** Get only error messages *)
108
-
val get_errors : t -> validation_issue list
323
+
(** Get only validation errors (specification violations).
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.
328
+
@param gpx The GPX document to validate
329
+
@return List of error-level validation issues *)
330
+
val errors : t -> validation_issue list
110
-
(** Get only warning messages *)
111
-
val get_warnings : t -> validation_issue list
332
+
(** Get only validation warnings (best practice violations).
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.
337
+
@param gpx The GPX document to validate
338
+
@return List of warning-level validation issues *)
339
+
val warnings : t -> validation_issue list
113
-
(** Format validation issue for display *)
341
+
(** Format a validation issue for human-readable display.
343
+
Combines the issue message with location context if available.
345
+
@param issue The validation issue to format
346
+
@return Formatted string suitable for display to users
348
+
Example output: ["Error in waypoint 1: Latitude out of range (-95.0)"] *)
val format_issue : validation_issue -> string
116
-
(** {1 Constructors and Utilities} *)
351
+
(** {1 Document Constructors and Utilities}
353
+
Functions for creating GPX documents and basic document operations. *)
118
-
(** Create new GPX document with required fields *)
355
+
(** Create a new GPX document with the required creator field.
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.
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
367
+
@param creator Name of the creating application (e.g., "MyGPS App v1.0")
368
+
@return Empty GPX document ready for data addition
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 *)
val make_gpx : creator:string -> t
121
-
(** Create empty GPX document *)
122
-
val empty : creator:string -> t
379
+
(** Create an empty GPX document with the required creator field.
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.
385
+
@param creator Name of the creating application
386
+
@return Empty GPX document
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");
395
+
val empty : creator:string -> t
397
+
(** {1 Common Patterns and Best Practices}
399
+
{2 Reading GPX Files}
401
+
The most common use case is reading existing GPX files:
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
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
414
+
{2 Creating GPX Files}
416
+
To create new GPX files with waypoints:
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
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
427
+
(* Create document *)
428
+
let gpx = make_gpx ~creator:"My App v1.0"
429
+
|> Doc.add_waypoint waypoint in
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)
437
+
{2 Working with Tracks}
439
+
Tracks represent recorded GPS traces with timestamped points:
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)
448
+
(* Create track segment *)
449
+
let segment = Track.Segment.make points in
452
+
let track = Track.make ~name:"Morning Run"
453
+
|> Track.add_segment segment in
455
+
(* Add to document *)
456
+
let gpx = make_gpx ~creator:"Fitness App"
457
+
|> Doc.add_track track
460
+
{2 Coordinate Systems and Units}
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)
468
+
{2 Validation Recommendations}
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
475
+
{2 Performance Considerations}
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
482
+
{2 Extension Support}
484
+
The library supports GPX extensions for custom data:
486
+
(* Create extension *)
487
+
let ext = Extension.make_text
488
+
~name:"temperature"
489
+
~namespace:"http://example.com/weather"
492
+
(* Add to waypoint *)
493
+
let waypoint = Waypoint.make lat lon
494
+
|> Waypoint.add_extensions [ext]
497
+
(** {1 Related Modules and Libraries}
499
+
This core module provides the foundation. For complete applications, consider:
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
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} *)