GPS Exchange Format library/CLI in OCaml

Compare changes

Choose any two refs to compare.

+35 -4
.tangled/workflows/build.yml
···
dependencies:
nixpkgs:
-
- dune
-
- ocamlPackages.xmlm
+
- shell
+
- stdenv
+
- findutils
+
- binutils
+
- libunwind
+
- ncurses
+
- opam
+
- git
+
- gawk
+
- gnupatch
+
- gnum4
+
- gnumake
+
- gnutar
+
- gnused
+
- gnugrep
+
- diffutils
+
- gzip
+
- bzip2
+
- gcc
+
- ocaml
steps:
-
- name: dune
+
- name: opam
+
command: |
+
opam init --disable-sandboxing -any
+
- name: switch
+
command: |
+
opam install . --confirm-level=unsafe-yes --deps-only
+
- name: build
+
command: |
+
opam exec -- dune build --verbose
+
- name: test
+
command: |
+
opam exec -- dune runtest --verbose
+
- name: doc
command: |
-
dune build
+
opam install -y odoc
+
opam exec -- dune build @doc
+3
CHANGES.md
···
+
# v1.0.0
+
+
- Initial public release
+6 -2
CLAUDE.md
···
-
My goal is to build a high quality GPX (GPS Exchange Format) in OCaml, using the xmlm streaming code library to make the core library portable. Then build a Unix-based layer over that to implement the IO.
+
My goal is to build a high quality GPX (GPS Exchange Format) in OCaml, using
+
the xmlm streaming code library to make the core library portable. Then build a
+
Unix-based layer over that to implement the IO.
-
The GPX homepage is at https://www.topografix.com/gpx.asp with the XSD scheme at https://www.topografix.com/GPX/1/1/gpx.xsd and the docs at https://www.topografix.com/GPX/1/1/
+
The GPX homepage is at https://www.topografix.com/gpx.asp with the XSD scheme
+
at https://www.topografix.com/GPX/1/1/gpx.xsd and the docs at
+
https://www.topografix.com/GPX/1/1/
+18
LICENSE.md
···
+
(*
+
* ISC License
+
*
+
* Copyright (c) 2025 Anil Madhavapeddy <anil@recoil.org>
+
*
+
* Permission to use, copy, modify, and distribute this software for any
+
* purpose with or without fee is hereby granted, provided that the above
+
* copyright notice and this permission notice appear in all copies.
+
*
+
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
*
+
*)
+57 -57
README.md
···
-
# mlgpx - OCaml GPX Library
+
# mlgpx - an OCaml GPS Exchange Format (GPX) Library
An OCaml library for parsing and generating GPX (GPS Exchange Format) 1.0 and
1.1 files, and a CLI for common manipulation and query options.
+
## Command Line Usage
+
+
The `mlgpx` CLI provides tools for manipulating GPX files from the command line.
+
+
### Installation
+
+
```bash
+
# Install from source
+
dune build @install
+
dune install
+
+
# Or use opam
+
opam install mlgpx
+
```
+
+
### Convert Waypoints to Track
+
+
```bash
+
# Basic conversion
+
mlgpx convert waypoints.gpx track.gpx
+
+
# With custom track name
+
mlgpx convert --name "My Route" waypoints.gpx route.gpx
+
+
# Sort waypoints by timestamp before conversion
+
mlgpx convert --sort-time waypoints.gpx sorted_track.gpx
+
+
# Sort by name and preserve original waypoints
+
mlgpx convert --sort-name --preserve waypoints.gpx mixed.gpx
+
+
# Verbose output with description
+
mlgpx convert --verbose --desc "Generated route" waypoints.gpx track.gpx
+
```
+
+
### File Analysis
+
+
```bash
+
# Basic file information
+
mlgpx info file.gpx
+
+
# Detailed analysis with waypoint details
+
mlgpx info --verbose file.gpx
+
```
+
+
### Help
+
+
```bash
+
# General help
+
mlgpx --help
+
+
# Command-specific help
+
mlgpx convert --help
+
mlgpx info --help
+
```
+
## Architecture Overview
The library is split into four main components:
### Core Library (`gpx`)
-
- **Portable**: No Unix dependencies, works with js_of_ocaml
+
- **Portable**: No Unix dependencies, works with `js_of_ocaml`
- **Streaming**: Uses xmlm for memory-efficient XML processing
- **Type-safe**: Strong typing with validation for coordinates and GPS data
- **Pure functional**: No side effects in the core parsing/writing logic
···
let () = create_simple_gpx ()
```
-
-
## Command Line Usage
-
-
The `mlgpx` CLI provides tools for manipulating GPX files from the command line.
-
-
### Installation
-
-
```bash
-
# Install from source
-
dune build @install
-
dune install
-
-
# Or use opam (when published)
-
opam install mlgpx
-
```
-
-
### Convert Waypoints to Track
-
-
```bash
-
# Basic conversion
-
mlgpx convert waypoints.gpx track.gpx
-
-
# With custom track name
-
mlgpx convert --name "My Route" waypoints.gpx route.gpx
-
-
# Sort waypoints by timestamp before conversion
-
mlgpx convert --sort-time waypoints.gpx sorted_track.gpx
-
-
# Sort by name and preserve original waypoints
-
mlgpx convert --sort-name --preserve waypoints.gpx mixed.gpx
-
-
# Verbose output with description
-
mlgpx convert --verbose --desc "Generated route" waypoints.gpx track.gpx
-
```
-
-
### File Analysis
-
-
```bash
-
# Basic file information
-
mlgpx info file.gpx
-
-
# Detailed analysis with waypoint details
-
mlgpx info --verbose file.gpx
-
```
-
-
### Help
-
-
```bash
-
# General help
-
mlgpx --help
-
-
# Command-specific help
-
mlgpx convert --help
-
mlgpx info --help
-
```
+1 -1
bin/dune
···
(executable
(public_name mlgpx)
(name mlgpx_cli)
-
(libraries gpx gpx_eio cmdliner eio_main fmt fmt.tty fmt.cli))
+
(libraries gpx gpx_eio cmdliner eio_main fmt fmt.tty fmt.cli))
+73 -89
bin/mlgpx_cli.ml
···
if waypoints = [] then
[]
else
-
let track_points = List.map (fun (wpt : waypoint) -> (wpt :> track_point)) waypoints in
-
[{ trkpts = track_points; extensions = [] }]
+
Track.Segment.make waypoints :: []
let sort_waypoints sort_by_time sort_by_name waypoints =
if sort_by_time then
-
List.sort (fun (wpt1 : waypoint) (wpt2 : waypoint) ->
-
match wpt1.time, wpt2.time with
+
List.sort (fun wpt1 wpt2 ->
+
match Waypoint.time wpt1, Waypoint.time wpt2 with
| Some t1, Some t2 -> Ptime.compare t1 t2
| Some _, None -> -1
| None, Some _ -> 1
| None, None -> 0
) waypoints
else if sort_by_name then
-
List.sort (fun (wpt1 : waypoint) (wpt2 : waypoint) ->
-
match wpt1.name, wpt2.name with
+
List.sort (fun wpt1 wpt2 ->
+
match Waypoint.name wpt1, Waypoint.name wpt2 with
| Some n1, Some n2 -> String.compare n1 n2
| Some _, None -> -1
| None, Some _ -> 1
···
if verbose then
log_info "Found %d waypoints and %d existing tracks"
-
(List.length gpx.waypoints)
-
(List.length gpx.tracks);
+
(Doc.waypoint_count gpx)
+
(Doc.track_count gpx);
(* Check if we have waypoints to convert *)
-
if gpx.waypoints = [] then (
+
if Doc.waypoints gpx = [] then (
log_error "Input file contains no waypoints - nothing to convert";
exit 1
);
(* Sort waypoints if requested *)
-
let sorted_waypoints = sort_waypoints sort_by_time sort_by_name gpx.waypoints in
+
let sorted_waypoints = sort_waypoints sort_by_time sort_by_name (Doc.waypoints gpx) in
if verbose && (sort_by_time || sort_by_name) then
log_info "Sorted %d waypoints" (List.length sorted_waypoints);
···
let track_segments = waypoints_to_track_segments sorted_waypoints in
(* Create the new track *)
-
let new_track = {
-
name = Some track_name;
+
let new_track = Track.make ~name:track_name in
+
let new_track = { new_track with
cmt = Some "Generated from waypoints by mlgpx CLI";
desc = track_desc;
src = Some "mlgpx";
-
links = [];
-
number = None;
type_ = Some "converted";
-
extensions = [];
trksegs = track_segments;
} in
if verbose then (
-
let total_points = List.fold_left (fun acc seg -> acc + List.length seg.trkpts) 0 track_segments in
+
let total_points = List.fold_left (fun acc seg -> acc + Track.Segment.point_count seg) 0 track_segments in
log_info "Created track %a with %d segments containing %d points"
(bold_style Fmt.string) track_name
(List.length track_segments) total_points
);
(* Build output GPX *)
-
let output_gpx = {
-
gpx with
-
waypoints = (if preserve_waypoints then gpx.waypoints else []);
-
tracks = new_track :: gpx.tracks;
-
metadata = (match gpx.metadata with
-
| Some meta -> Some { meta with
-
desc = Some (match meta.desc with
+
let output_gpx =
+
if preserve_waypoints then
+
Doc.add_track gpx new_track
+
else
+
Doc.add_track (Doc.clear_waypoints gpx) new_track in
+
let output_gpx =
+
match Doc.metadata gpx with
+
| Some meta ->
+
let desc = match Metadata.description meta with
| Some existing -> existing ^ " (waypoints converted to track)"
-
| None -> "Waypoints converted to track") }
-
| None -> Some { empty_metadata with
-
desc = Some "Waypoints converted to track";
-
time = None })
-
} in
+
| None -> "Waypoints converted to track" in
+
Doc.with_metadata output_gpx { meta with desc = Some desc }
+
| None ->
+
let meta = Metadata.make ~name:"Converted" in
+
let meta = { meta with desc = Some "Waypoints converted to track" } in
+
Doc.with_metadata output_gpx meta in
(* Validate output *)
-
let validation = validate_gpx output_gpx in
+
let validation = Gpx.validate_gpx output_gpx in
if not validation.is_valid then (
log_error "Generated GPX failed validation:";
-
List.iter (fun issue ->
+
List.iter (fun (issue : Gpx.validation_issue) ->
let level_str = match issue.level with `Error -> "ERROR" | `Warning -> "WARNING" in
let level_color = match issue.level with `Error -> error_style | `Warning -> warn_style in
Fmt.pf Format.err_formatter " %a: %s\n" (level_color Fmt.string) level_str issue.message
···
Fmt.pf Format.std_formatter "%a\n" (success_style Fmt.string) "Conversion completed successfully!";
log_info "Output contains:";
Fmt.pf Format.err_formatter " - %d waypoints%s\n"
-
(List.length output_gpx.waypoints)
+
(Doc.waypoint_count output_gpx)
(if preserve_waypoints then " (preserved)" else " (removed)");
Fmt.pf Format.err_formatter " - %d tracks (%a + %d existing)\n"
-
(List.length output_gpx.tracks)
+
(Doc.track_count output_gpx)
(success_style Fmt.string) "1 new"
-
(List.length gpx.tracks)
+
(Doc.track_count gpx)
) else (
log_success "Converted %d waypoints to track: %a → %a"
(List.length sorted_waypoints)
···
with
| Gpx.Gpx_error err ->
-
log_error "GPX Error: %s" (match err with
-
| Invalid_xml s -> "Invalid XML: " ^ s
-
| Invalid_coordinate s -> "Invalid coordinate: " ^ s
-
| Missing_required_attribute (elem, attr) ->
-
Printf.sprintf "Missing attribute %s in %s" attr elem
-
| Missing_required_element s -> "Missing element: " ^ s
-
| Validation_error s -> "Validation error: " ^ s
-
| Xml_error s -> "XML error: " ^ s
-
| IO_error s -> "I/O error: " ^ s);
+
log_error "GPX Error: %s" (Error.to_string err);
exit 2
| Sys_error msg ->
log_error "System error: %s" msg;
···
let times = ref [] in
(* Collect from waypoints *)
-
List.iter (fun (wpt : waypoint) ->
-
match wpt.time with
+
List.iter (fun wpt ->
+
match Waypoint.time wpt with
| Some t -> times := t :: !times
| None -> ()
-
) gpx.waypoints;
+
) (Doc.waypoints gpx);
(* Collect from routes *)
List.iter (fun route ->
-
List.iter (fun (rtept : route_point) ->
-
match rtept.time with
+
List.iter (fun rtept ->
+
match Waypoint.time rtept with
| Some t -> times := t :: !times
| None -> ()
-
) route.rtepts
-
) gpx.routes;
+
) (Route.points route)
+
) (Doc.routes gpx);
(* Collect from tracks *)
List.iter (fun track ->
List.iter (fun seg ->
-
List.iter (fun (trkpt : track_point) ->
-
match trkpt.time with
+
List.iter (fun trkpt ->
+
match Waypoint.time trkpt with
| Some t -> times := t :: !times
| None -> ()
-
) seg.trkpts
-
) track.trksegs
-
) gpx.tracks;
+
) (Track.Segment.points seg)
+
) (Track.segments track)
+
) (Doc.tracks gpx);
!times
···
Fmt.pf Format.std_formatter "%a\n" (bold_style Fmt.string) "GPX File Information";
(* Basic info *)
-
Printf.printf " Version: %s\n" gpx.version;
-
Printf.printf " Creator: %s\n" gpx.creator;
+
Printf.printf " Version: %s\n" (Doc.version gpx);
+
Printf.printf " Creator: %s\n" (Doc.creator gpx);
-
(match gpx.metadata with
+
(match Doc.metadata gpx with
| Some meta ->
-
Printf.printf " Name: %s\n" (Option.value meta.name ~default:"<unnamed>");
-
Printf.printf " Description: %s\n" (Option.value meta.desc ~default:"<none>");
-
(match meta.time with
+
Printf.printf " Name: %s\n" (Option.value (Metadata.name meta) ~default:"<unnamed>");
+
Printf.printf " Description: %s\n" (Option.value (Metadata.description meta) ~default:"<none>");
+
(match Metadata.time meta with
| Some time -> Printf.printf " Created: %s\n" (Ptime.to_rfc3339 time)
| None -> ())
| None ->
···
(* Content summary *)
Fmt.pf Format.std_formatter "\n%a\n" (bold_style Fmt.string) "Content Summary";
-
Printf.printf " Waypoints: %d\n" (List.length gpx.waypoints);
-
Printf.printf " Routes: %d\n" (List.length gpx.routes);
-
Printf.printf " Tracks: %d\n" (List.length gpx.tracks);
+
Printf.printf " Waypoints: %d\n" (Doc.waypoint_count gpx);
+
Printf.printf " Routes: %d\n" (Doc.route_count gpx);
+
Printf.printf " Tracks: %d\n" (Doc.track_count gpx);
(* Time range *)
let all_times = collect_all_timestamps gpx in
···
);
(* Detailed waypoint info *)
-
if gpx.waypoints <> [] then (
+
if Doc.waypoints gpx <> [] then (
Fmt.pf Format.std_formatter "\n%a\n" (bold_style Fmt.string) "Waypoints";
-
let waypoints_with_time = List.filter (fun (wpt : waypoint) -> wpt.time <> None) gpx.waypoints in
-
let waypoints_with_elevation = List.filter (fun (wpt : waypoint) -> wpt.ele <> None) gpx.waypoints in
+
let waypoints_with_time = List.filter (fun wpt -> Waypoint.time wpt <> None) (Doc.waypoints gpx) in
+
let waypoints_with_elevation = List.filter (fun wpt -> Waypoint.elevation wpt <> None) (Doc.waypoints gpx) in
Printf.printf " - %d with timestamps\n" (List.length waypoints_with_time);
Printf.printf " - %d with elevation data\n" (List.length waypoints_with_elevation);
-
if verbose && List.length gpx.waypoints <= 10 then (
+
if verbose && List.length (Doc.waypoints gpx) <= 10 then (
Printf.printf " Details:\n";
-
List.iteri (fun i (wpt : waypoint) ->
+
List.iteri (fun i wpt ->
+
let lat, lon = Waypoint.to_floats wpt in
Fmt.pf Format.std_formatter " %a %s (%.6f, %.6f)%s%s\n"
(info_style Fmt.string) (Printf.sprintf "%d." (i + 1))
-
(Option.value wpt.name ~default:"<unnamed>")
-
(latitude_to_float wpt.lat) (longitude_to_float wpt.lon)
-
(match wpt.ele with Some e -> Printf.sprintf " elev=%.1fm" e | None -> "")
-
(match wpt.time with Some t -> " @" ^ Ptime.to_rfc3339 t | None -> "")
-
) gpx.waypoints
+
(Option.value (Waypoint.name wpt) ~default:"<unnamed>")
+
lat lon
+
(match Waypoint.elevation wpt with Some e -> Printf.sprintf " elev=%.1fm" e | None -> "")
+
(match Waypoint.time wpt with Some t -> " @" ^ Ptime.to_rfc3339 t | None -> "")
+
) (Doc.waypoints gpx)
)
);
(* Track info *)
-
if gpx.tracks <> [] then (
+
if Doc.tracks gpx <> [] then (
Fmt.pf Format.std_formatter "\n%a\n" (bold_style Fmt.string) "Tracks";
List.iteri (fun i track ->
-
let total_points = List.fold_left (fun acc seg -> acc + List.length seg.trkpts) 0 track.trksegs in
+
let total_points = Track.point_count track in
Fmt.pf Format.std_formatter " %a %s (%d segments, %d points)\n"
(info_style Fmt.string) (Printf.sprintf "%d." (i + 1))
-
(Option.value track.name ~default:"<unnamed>")
-
(List.length track.trksegs) total_points
-
) gpx.tracks
+
(Option.value (Track.name track) ~default:"<unnamed>")
+
(Track.segment_count track) total_points
+
) (Doc.tracks gpx)
);
(* Validation *)
-
let validation = validate_gpx gpx in
+
let validation = Gpx.validate_gpx gpx in
Printf.printf "\n";
if validation.is_valid then
Fmt.pf Format.std_formatter "Validation: %a\n" (success_style Fmt.string) "PASSED"
else (
Fmt.pf Format.std_formatter "Validation: %a\n" (error_style Fmt.string) "FAILED";
-
List.iter (fun issue ->
+
List.iter (fun (issue : Gpx.validation_issue) ->
let level_str = match issue.level with `Error -> "ERROR" | `Warning -> "WARNING" in
let level_color = match issue.level with `Error -> error_style | `Warning -> warn_style in
Fmt.pf Format.std_formatter " %a: %s\n" (level_color Fmt.string) level_str issue.message
···
with
| Gpx.Gpx_error err ->
-
log_error "GPX Error: %s" (match err with
-
| Invalid_xml s -> "Invalid XML: " ^ s
-
| Invalid_coordinate s -> "Invalid coordinate: " ^ s
-
| Missing_required_attribute (elem, attr) ->
-
Printf.sprintf "Missing attribute %s in %s" attr elem
-
| Missing_required_element s -> "Missing element: " ^ s
-
| Validation_error s -> "Validation error: " ^ s
-
| Xml_error s -> "XML error: " ^ s
-
| IO_error s -> "I/O error: " ^ s);
+
log_error "GPX Error: %s" (Error.to_string err);
exit 2
| Sys_error msg ->
log_error "System error: %s" msg;
+14 -5
dune-project
···
-
(lang dune 3.15)
+
(lang dune 3.18)
+
+
(name mlgpx)
+
+
(generate_opam_files true)
(package
(name mlgpx)
-
(depends ocaml dune xmlm ptime eio ppx_expect alcotest eio_main cmdliner fmt logs)
-
(synopsis "OCaml library for parsing and generating GPX files")
+
(depends ocaml dune xmlm ptime (eio (>= 1.2)) ppx_expect alcotest eio_main cmdliner fmt logs)
+
(synopsis "Library and CLI for parsing and generating GPS Exchange (GPX) formats")
(description
"mlgpx is a streaming GPX (GPS Exchange Format) library for OCaml. It provides a portable core library using the xmlm streaming XML parser, with a separate Unix layer for file I/O operations. The library supports the complete GPX 1.1 specification including waypoints, routes, tracks, and metadata with strong type safety and validation.")
-
(license MIT)
-
(authors "Anil Madhavapeddy"))
+
(license ISC)
+
(authors "Anil Madhavapeddy")
+
(homepage "https://tangled.sh/@anil.recoil.org/ocaml-gpx")
+
(maintainers "Anil Madhavapeddy <anil@recoil.org>")
+
(bug_reports https://tangled.sh/@anil.recoil.org/ocaml-gpx/issues)
+
(maintenance_intent "(latest)")
+
)
+1 -7
examples/dune
···
(executable
(public_name simple_gpx)
(name simple_gpx)
-
(libraries gpx_unix))
-
-
(executable
-
(public_name effects_example)
-
(name effects_example)
-
(libraries gpx_eio eio_main)
-
(optional))
+
(libraries gpx xmlm))
-88
examples/effects_example.ml
···
-
(** Example using GPX with real Eio effects-based API
-
-
This demonstrates the real Eio-based API with structured concurrency
-
and proper resource management.
-
**)
-
-
open Gpx_eio
-
-
let main env =
-
try
-
let fs = Eio.Stdenv.fs env in
-
-
(* Create some GPS coordinates *)
-
let lat1 = Gpx.latitude 37.7749 |> Result.get_ok in
-
let lon1 = Gpx.longitude (-122.4194) |> Result.get_ok in
-
let lat2 = Gpx.latitude 37.7849 |> Result.get_ok in
-
let lon2 = Gpx.longitude (-122.4094) |> Result.get_ok in
-
-
(* Create waypoints *)
-
let waypoint1 = make_waypoint ~fs ~lat:(Gpx.latitude_to_float lat1) ~lon:(Gpx.longitude_to_float lon1) ~name:"San Francisco" () in
-
let waypoint2 = make_waypoint ~fs ~lat:(Gpx.latitude_to_float lat2) ~lon:(Gpx.longitude_to_float lon2) ~name:"Near SF" () in
-
-
(* Create a simple track from coordinates *)
-
let track = make_track_from_coords ~fs ~name:"SF Walk" [
-
(37.7749, -122.4194);
-
(37.7759, -122.4184);
-
(37.7769, -122.4174);
-
(37.7779, -122.4164);
-
] in
-
-
(* Create a route *)
-
let route = make_route_from_coords ~fs ~name:"SF Route" [
-
(37.7749, -122.4194);
-
(37.7849, -122.4094);
-
] in
-
-
(* Create GPX document with all elements *)
-
let gpx = Gpx.make_gpx ~creator:"eio-example" in
-
let gpx = { gpx with
-
waypoints = [waypoint1; waypoint2];
-
tracks = [track];
-
routes = [route];
-
} in
-
-
Printf.printf "Created GPX document with:\\n";
-
print_stats gpx;
-
Printf.printf "\\n";
-
-
(* Write to file with validation *)
-
write_validated ~fs "example_output.gpx" gpx;
-
Printf.printf "Wrote GPX to example_output.gpx\\n";
-
-
(* Read it back and verify *)
-
let gpx2 = read_validated ~fs "example_output.gpx" in
-
Printf.printf "Read back GPX document with %d waypoints, %d tracks, %d routes\\n"
-
(List.length gpx2.waypoints) (List.length gpx2.tracks) (List.length gpx2.routes);
-
-
(* Extract coordinates from track *)
-
match gpx2.tracks with
-
| track :: _ ->
-
let coords = track_coords track in
-
Printf.printf "Track coordinates: %d points\\n" (List.length coords);
-
List.iteri (fun i (lat, lon) ->
-
Printf.printf " Point %d: %.4f, %.4f\\n" i lat lon
-
) coords
-
| [] -> Printf.printf "No tracks found\\n";
-
-
Printf.printf "\\nEio example completed successfully!\\n"
-
-
with
-
| Gpx.Gpx_error err ->
-
let error_msg = match err with
-
| Gpx.Invalid_xml s -> "Invalid XML: " ^ s
-
| Gpx.Invalid_coordinate s -> "Invalid coordinate: " ^ s
-
| Gpx.Missing_required_attribute (elem, attr) ->
-
Printf.sprintf "Missing required attribute '%s' in element '%s'" attr elem
-
| Gpx.Missing_required_element s -> "Missing required element: " ^ s
-
| Gpx.Validation_error s -> "Validation error: " ^ s
-
| Gpx.Xml_error s -> "XML error: " ^ s
-
| Gpx.IO_error s -> "I/O error: " ^ s
-
in
-
Printf.eprintf "GPX Error: %s\\n" error_msg;
-
exit 1
-
| exn ->
-
Printf.eprintf "Unexpected error: %s\\n" (Printexc.to_string exn);
-
exit 1
-
-
let () = Eio_main.run main
+87 -104
examples/simple_gpx.ml
···
(* Create coordinates using direct API *)
let create_coordinate_pair lat_f lon_f =
-
match latitude lat_f, longitude lon_f with
+
match Coordinate.latitude lat_f, Coordinate.longitude lon_f with
| Ok lat, Ok lon -> Ok (lat, lon)
-
| Error e, _ | _, Error e -> Error (Invalid_coordinate e)
+
| Error e, _ | _, Error e -> Error (Error.invalid_coordinate e)
in
(* Create a simple waypoint *)
-
(match create_coordinate_pair 37.7749 (-122.4194) with
-
| Ok (lat, lon) ->
-
let wpt = make_waypoint_data lat lon in
-
let wpt = { wpt with name = Some "San Francisco"; desc = Some "Golden Gate Bridge area" } in
-
Printf.printf "✓ Created waypoint: %s\n" (Option.value wpt.name ~default:"<unnamed>");
-
-
(* Create GPX document *)
-
let gpx = make_gpx ~creator:"mlgpx direct API example" in
-
let gpx = { gpx with waypoints = [wpt] } in
-
-
(* Add metadata *)
-
let metadata = { empty_metadata with
-
name = Some "Example GPX File";
-
desc = Some "Demonstration of mlgpx library capabilities";
-
time = None (* Ptime_clock not available in this context *)
-
} in
-
let gpx = { gpx with metadata = Some metadata } in
-
-
(* Create a simple track *)
-
let track_points = [
-
(37.7749, -122.4194, Some "Start");
-
(37.7849, -122.4094, Some "Mid Point");
-
(37.7949, -122.3994, Some "End");
-
] in
-
-
let create_track_points acc (lat_f, lon_f, name) =
-
match create_coordinate_pair lat_f lon_f with
-
| Ok (lat, lon) ->
-
let trkpt = make_waypoint_data lat lon in
-
let trkpt = { trkpt with name } in
-
trkpt :: acc
-
| Error _ -> acc
-
in
-
-
let trkpts = List.fold_left create_track_points [] track_points |> List.rev in
-
let trkseg = { trkpts; extensions = [] } in
-
let track = {
-
name = Some "Example Track";
-
cmt = Some "Sample GPS track";
-
desc = Some "Demonstrates track creation";
-
src = None; links = []; number = None; type_ = None; extensions = [];
-
trksegs = [trkseg];
-
} in
-
let gpx = { gpx with tracks = [track] } in
-
-
Printf.printf "✓ Created track with %d points\n" (List.length trkpts);
-
-
(* Validate the document *)
-
let validation = validate_gpx gpx in
-
Printf.printf "✓ GPX validation: %s\n" (if validation.is_valid then "PASSED" else "FAILED");
-
-
if not validation.is_valid then (
-
Printf.printf "Validation issues:\n";
-
List.iter (fun issue ->
-
Printf.printf " %s: %s\n"
-
(match issue.level with `Error -> "ERROR" | `Warning -> "WARNING")
-
issue.message
-
) validation.issues
-
);
-
-
(* Convert to XML string *)
-
(match write_string gpx with
-
| Ok xml_string ->
-
Printf.printf "✓ Generated XML (%d characters)\n" (String.length xml_string);
-
-
(* Save to file using Unix layer for convenience *)
-
(match Gpx_unix.write_validated "example_direct.gpx" gpx with
-
| Ok () ->
-
Printf.printf "✓ Saved to example_direct.gpx\n";
-
-
(* Read it back to verify round-trip *)
-
(match Gpx_unix.read_validated "example_direct.gpx" with
-
| Ok gpx2 ->
-
Printf.printf "✓ Successfully read back GPX\n";
-
let validation2 = validate_gpx gpx2 in
-
Printf.printf "✓ Round-trip validation: %s\n"
-
(if validation2.is_valid then "PASSED" else "FAILED");
-
Printf.printf " Waypoints: %d, Tracks: %d\n"
-
(List.length gpx2.waypoints) (List.length gpx2.tracks)
-
| Error e ->
-
Printf.printf "✗ Error reading back: %s\n"
-
(match e with
-
| Invalid_xml s -> "Invalid XML: " ^ s
-
| Validation_error s -> "Validation: " ^ s
-
| IO_error s -> "I/O: " ^ s
-
| _ -> "Unknown error"))
-
| Error e ->
-
Printf.printf "✗ Error saving file: %s\n"
-
(match e with
-
| IO_error s -> s
-
| Validation_error s -> s
-
| _ -> "Unknown error"))
-
| Error e ->
-
Printf.printf "✗ Error generating XML: %s\n"
-
(match e with
-
| Invalid_xml s -> s
-
| Xml_error s -> s
-
| _ -> "Unknown error"))
-
| Error e ->
-
Printf.printf "✗ Error creating coordinates: %s\n"
-
(match e with Invalid_coordinate s -> s | _ -> "Unknown error"));
+
let result = create_coordinate_pair 37.7749 (-122.4194) in
+
match result with
+
| Ok (lat, lon) ->
+
let wpt = Waypoint.make lat lon in
+
let wpt = { wpt with name = Some "San Francisco"; desc = Some "Golden Gate Bridge area" } in
+
Printf.printf "✓ Created waypoint: %s\n" (Option.value (Waypoint.name wpt) ~default:"<unnamed>");
+
+
(* Create GPX document *)
+
let gpx = Doc.empty ~creator:"mlgpx direct API example" in
+
let gpx = Doc.add_waypoint gpx wpt in
+
+
(* Add metadata *)
+
let metadata = Metadata.empty in
+
let metadata = { metadata with name = Some "Example GPX File"; desc = Some "Demonstration of mlgpx library capabilities" } in
+
let gpx = { gpx with metadata = Some metadata } in
+
+
(* Create a simple track with points *)
+
let track = Track.make ~name:"Example Track" in
+
let track = { track with cmt = Some "Sample GPS track"; desc = Some "Demonstrates track creation" } in
+
+
(* Create track segment with points *)
+
let track_segment = Track.Segment.empty in
+
let points = [
+
(37.7749, -122.4194);
+
(37.7849, -122.4094);
+
(37.7949, -122.3994);
+
] in
+
let track_segment =
+
List.fold_left (fun seg (lat_f, lon_f) ->
+
match Coordinate.latitude lat_f, Coordinate.longitude lon_f with
+
| Ok lat, Ok lon ->
+
let pt = Waypoint.make lat lon in
+
Track.Segment.add_point seg pt
+
| _ -> seg
+
) track_segment points in
+
+
let track = Track.add_segment track track_segment in
+
let gpx = Doc.add_track gpx track in
+
+
Printf.printf "✓ Created track\n";
+
+
(* Validate the document *)
+
let validation = validate_gpx gpx in
+
Printf.printf "✓ GPX validation: %s\n" (if validation.is_valid then "PASSED" else "FAILED");
+
+
(* Convert to XML string *)
+
let xml_result = write_string gpx in
+
(match xml_result with
+
| Ok xml_string ->
+
Printf.printf "✓ Generated XML (%d characters)\n" (String.length xml_string);
+
+
(* Save to file - write directly using core API *)
+
let out_chan = open_out "example_direct.gpx" in
+
let dest = (`Channel out_chan) in
+
let write_result = write ~validate:true dest gpx in
+
close_out out_chan;
+
(match write_result with
+
| Ok () ->
+
Printf.printf "✓ Saved to example_direct.gpx\n";
+
+
(* Read it back to verify round-trip *)
+
let in_chan = open_in "example_direct.gpx" in
+
let input = Xmlm.make_input (`Channel in_chan) in
+
let read_result = parse ~validate:true input in
+
close_in in_chan;
+
(match read_result with
+
| Ok gpx2 ->
+
Printf.printf "✓ Successfully read back GPX\n";
+
let validation2 = validate_gpx gpx2 in
+
Printf.printf "✓ Round-trip validation: %s\n"
+
(if validation2.is_valid then "PASSED" else "FAILED");
+
Printf.printf " Waypoints: %d, Tracks: %d\n"
+
(List.length (Doc.waypoints gpx2)) (List.length (Doc.tracks gpx2))
+
| Error e ->
+
Printf.printf "✗ Error reading back: %s\n" (Error.to_string e)
+
)
+
| Error e ->
+
Printf.printf "✗ Error saving file: %s\n" (Error.to_string e)
+
)
+
| Error e ->
+
Printf.printf "✗ Error generating XML: %s\n" (Error.to_string e)
+
)
+
| Error e ->
+
Printf.printf "✗ Error creating coordinates: %s\n" (Error.to_string e);
-
Printf.printf "\n=== Example Complete ===\n"
+
Printf.printf "\n=== Example Complete ===\n"
+59
lib/gpx/coordinate.ml
···
+
(** Geographic coordinate types with validation *)
+
+
(** Private coordinate types with validation constraints *)
+
type latitude = private float
+
type longitude = private float
+
type degrees = private float
+
+
(** Coordinate pair - main type for this module *)
+
type t = {
+
lat : latitude;
+
lon : longitude;
+
}
+
+
(** Smart constructors for validated coordinates *)
+
let latitude f =
+
if f >= -90.0 && f <= 90.0 then Ok (Obj.magic f : latitude)
+
else Error (Printf.sprintf "Invalid latitude: %f (must be between -90.0 and 90.0)" f)
+
+
let longitude f =
+
if f >= -180.0 && f < 180.0 then Ok (Obj.magic f : longitude)
+
else Error (Printf.sprintf "Invalid longitude: %f (must be between -180.0 and 180.0)" f)
+
+
let degrees f =
+
if f >= 0.0 && f < 360.0 then Ok (Obj.magic f : degrees)
+
else Error (Printf.sprintf "Invalid degrees: %f (must be between 0.0 and 360.0)" f)
+
+
(** Convert back to float *)
+
let latitude_to_float (lat : latitude) = (lat :> float)
+
let longitude_to_float (lon : longitude) = (lon :> float)
+
let degrees_to_float (deg : degrees) = (deg :> float)
+
+
(** Create coordinate pair *)
+
let make lat lon = { lat; lon }
+
+
(** Create coordinate pair from floats with validation *)
+
let make_from_floats lat_f lon_f =
+
match latitude lat_f, longitude lon_f with
+
| Ok lat, Ok lon -> Ok { lat; lon }
+
| Error e, _ | _, Error e -> Error e
+
+
(** Extract components *)
+
let lat t = t.lat
+
let lon t = t.lon
+
let to_floats t = (latitude_to_float t.lat, longitude_to_float t.lon)
+
+
(** Compare coordinates *)
+
let compare t1 t2 =
+
let lat_cmp = Float.compare (latitude_to_float t1.lat) (latitude_to_float t2.lat) in
+
if lat_cmp <> 0 then lat_cmp
+
else Float.compare (longitude_to_float t1.lon) (longitude_to_float t2.lon)
+
+
(** Equality *)
+
let equal t1 t2 = compare t1 t2 = 0
+
+
(** Pretty printer *)
+
let pp ppf t =
+
Format.fprintf ppf "(%g, %g)"
+
(latitude_to_float t.lat)
+
(longitude_to_float t.lon)
+68
lib/gpx/coordinate.mli
···
+
(** Geographic coordinate types with validation *)
+
+
(** Coordinate types with validation constraints *)
+
type latitude = private float
+
type longitude = private float
+
type degrees = private float
+
+
(** Coordinate pair *)
+
type t = {
+
lat : latitude;
+
lon : longitude;
+
}
+
+
(** {2 Constructors} *)
+
+
(** Create validated latitude.
+
@param f Latitude in degrees (-90.0 to 90.0)
+
@return [Ok latitude] or [Error msg] *)
+
val latitude : float -> (latitude, string) result
+
+
(** Create validated longitude.
+
@param f Longitude in degrees (-180.0 to 180.0)
+
@return [Ok longitude] or [Error msg] *)
+
val longitude : float -> (longitude, string) result
+
+
(** Create validated degrees.
+
@param f Degrees (0.0 to 360.0)
+
@return [Ok degrees] or [Error msg] *)
+
val degrees : float -> (degrees, string) result
+
+
(** {2 Conversion Functions} *)
+
+
(** Convert latitude to float *)
+
val latitude_to_float : latitude -> float
+
+
(** Convert longitude to float *)
+
val longitude_to_float : longitude -> float
+
+
(** Convert degrees to float *)
+
val degrees_to_float : degrees -> float
+
+
(** {2 Operations} *)
+
+
(** Create coordinate pair from validated components *)
+
val make : latitude -> longitude -> t
+
+
(** Create coordinate pair from floats with validation *)
+
val make_from_floats : float -> float -> (t, string) result
+
+
(** Extract latitude component *)
+
val lat : t -> latitude
+
+
(** Extract longitude component *)
+
val lon : t -> longitude
+
+
(** Convert coordinate to float pair *)
+
val to_floats : t -> float * float
+
+
(** {2 Comparison and Printers} *)
+
+
(** Compare two coordinates *)
+
val compare : t -> t -> int
+
+
(** Test coordinate equality *)
+
val equal : t -> t -> bool
+
+
(** Pretty print coordinate *)
+
val pp : Format.formatter -> t -> unit
+186
lib/gpx/doc.ml
···
+
(** Main GPX document type *)
+
+
(** Main GPX document type *)
+
type t = {
+
version : string; (* GPX version: "1.0" or "1.1" *)
+
creator : string; (* Creating application *)
+
metadata : Metadata.t option; (* Document metadata *)
+
waypoints : Waypoint.t list; (* Waypoints *)
+
routes : Route.t list; (* Routes *)
+
tracks : Track.t list; (* Tracks *)
+
extensions : Extension.t list; (* Document-level extensions *)
+
}
+
+
(** {2 Document Constructors} *)
+
+
(** Create empty GPX document *)
+
let empty ~creator = {
+
version = "1.1";
+
creator;
+
metadata = None;
+
waypoints = [];
+
routes = [];
+
tracks = [];
+
extensions = [];
+
}
+
+
(** Create GPX document with metadata *)
+
let make ~creator ~metadata =
+
{ (empty ~creator) with metadata = Some metadata }
+
+
(** {2 Document Properties} *)
+
+
(** Get version *)
+
let version t = t.version
+
+
(** Get creator *)
+
let creator t = t.creator
+
+
(** Get metadata *)
+
let metadata t = t.metadata
+
+
(** Get waypoints *)
+
let waypoints t = t.waypoints
+
+
(** Get routes *)
+
let routes t = t.routes
+
+
(** Get tracks *)
+
let tracks t = t.tracks
+
+
(** Get extensions *)
+
let extensions t = t.extensions
+
+
(** {2 Document Modification} *)
+
+
(** Update metadata *)
+
let with_metadata t metadata = { t with metadata = Some metadata }
+
+
(** Add waypoint *)
+
let add_waypoint t waypoint = { t with waypoints = t.waypoints @ [waypoint] }
+
+
(** Add waypoints *)
+
let add_waypoints t waypoints = { t with waypoints = t.waypoints @ waypoints }
+
+
(** Add route *)
+
let add_route t route = { t with routes = t.routes @ [route] }
+
+
(** Add routes *)
+
let add_routes t routes = { t with routes = t.routes @ routes }
+
+
(** Add track *)
+
let add_track t track = { t with tracks = t.tracks @ [track] }
+
+
(** Add tracks *)
+
let add_tracks t tracks = { t with tracks = t.tracks @ tracks }
+
+
(** Add extensions *)
+
let add_extensions t extensions = { t with extensions = t.extensions @ extensions }
+
+
(** Clear waypoints *)
+
let clear_waypoints t = { t with waypoints = [] }
+
+
(** Clear routes *)
+
let clear_routes t = { t with routes = [] }
+
+
(** Clear tracks *)
+
let clear_tracks t = { t with tracks = [] }
+
+
(** {2 Document Analysis} *)
+
+
(** Count waypoints *)
+
let waypoint_count t = List.length t.waypoints
+
+
(** Count routes *)
+
let route_count t = List.length t.routes
+
+
(** Count tracks *)
+
let track_count t = List.length t.tracks
+
+
(** Count total points *)
+
let total_points t =
+
let waypoint_points = List.length t.waypoints in
+
let route_points = List.fold_left (fun acc route ->
+
acc + Route.point_count route) 0 t.routes in
+
let track_points = List.fold_left (fun acc track ->
+
acc + Track.point_count track) 0 t.tracks in
+
waypoint_points + route_points + track_points
+
+
(** Check if document has elevation data *)
+
let has_elevation t =
+
List.exists (fun wpt -> Waypoint.elevation wpt <> None) t.waypoints ||
+
List.exists (fun route ->
+
List.exists (fun pt -> Waypoint.elevation pt <> None) (Route.points route)
+
) t.routes ||
+
List.exists (fun track ->
+
List.exists (fun pt -> Waypoint.elevation pt <> None) (Track.all_points track)
+
) t.tracks
+
+
(** Check if document has time data *)
+
let has_time t =
+
List.exists (fun wpt -> Waypoint.time wpt <> None) t.waypoints ||
+
List.exists (fun route ->
+
List.exists (fun pt -> Waypoint.time pt <> None) (Route.points route)
+
) t.routes ||
+
List.exists (fun track ->
+
List.exists (fun pt -> Waypoint.time pt <> None) (Track.all_points track)
+
) t.tracks
+
+
(** Check if document is empty *)
+
let is_empty t =
+
waypoint_count t = 0 && route_count t = 0 && track_count t = 0
+
+
(** Get statistics *)
+
type stats = {
+
waypoint_count : int;
+
route_count : int;
+
track_count : int;
+
total_points : int;
+
has_elevation : bool;
+
has_time : bool;
+
}
+
+
let stats t = {
+
waypoint_count = waypoint_count t;
+
route_count = route_count t;
+
track_count = track_count t;
+
total_points = total_points t;
+
has_elevation = has_elevation t;
+
has_time = has_time t;
+
}
+
+
(** Pretty print statistics *)
+
let pp_stats ppf t =
+
let s = stats t in
+
Format.fprintf ppf "@[<v>GPX Statistics:@, Waypoints: %d@, Routes: %d@, Tracks: %d@, Total points: %d@, Has elevation data: %s@, Has time data: %s@]"
+
s.waypoint_count s.route_count s.track_count s.total_points
+
(if s.has_elevation then "yes" else "no")
+
(if s.has_time then "yes" else "no")
+
+
(** {2 Comparison and Utilities} *)
+
+
(** Compare documents *)
+
let compare t1 t2 =
+
let version_cmp = String.compare t1.version t2.version in
+
if version_cmp <> 0 then version_cmp
+
else
+
let creator_cmp = String.compare t1.creator t2.creator in
+
if creator_cmp <> 0 then creator_cmp
+
else
+
let waypoints_cmp = List.compare Waypoint.compare t1.waypoints t2.waypoints in
+
if waypoints_cmp <> 0 then waypoints_cmp
+
else
+
let routes_cmp = List.compare Route.compare t1.routes t2.routes in
+
if routes_cmp <> 0 then routes_cmp
+
else List.compare Track.compare t1.tracks t2.tracks
+
+
(** Test document equality *)
+
let equal t1 t2 = compare t1 t2 = 0
+
+
(** Pretty print document *)
+
let pp ppf t =
+
let stats = stats t in
+
Format.fprintf ppf "GPX v%s by %s (%d wpt, %d routes, %d tracks, %d total points)"
+
t.version t.creator
+
stats.waypoint_count stats.route_count stats.track_count stats.total_points
+
+128
lib/gpx/doc.mli
···
+
(** Main GPX document type *)
+
+
(** Main GPX document type *)
+
type t = {
+
version : string; (* GPX version: "1.0" or "1.1" *)
+
creator : string; (* Creating application *)
+
metadata : Metadata.t option; (* Document metadata *)
+
waypoints : Waypoint.t list; (* Waypoints *)
+
routes : Route.t list; (* Routes *)
+
tracks : Track.t list; (* Tracks *)
+
extensions : Extension.t list; (* Document-level extensions *)
+
}
+
+
(** Document statistics *)
+
type stats = {
+
waypoint_count : int;
+
route_count : int;
+
track_count : int;
+
total_points : int;
+
has_elevation : bool;
+
has_time : bool;
+
}
+
+
(** {2 Document Constructors} *)
+
+
(** Create empty GPX document *)
+
val empty : creator:string -> t
+
+
(** Create GPX document with metadata *)
+
val make : creator:string -> metadata:Metadata.t -> t
+
+
(** {2 Document Properties} *)
+
+
(** Get version *)
+
val version : t -> string
+
+
(** Get creator *)
+
val creator : t -> string
+
+
(** Get metadata *)
+
val metadata : t -> Metadata.t option
+
+
(** Get waypoints *)
+
val waypoints : t -> Waypoint.t list
+
+
(** Get routes *)
+
val routes : t -> Route.t list
+
+
(** Get tracks *)
+
val tracks : t -> Track.t list
+
+
(** Get extensions *)
+
val extensions : t -> Extension.t list
+
+
(** {2 Document Modification} *)
+
+
(** Update metadata *)
+
val with_metadata : t -> Metadata.t -> t
+
+
(** Add waypoint *)
+
val add_waypoint : t -> Waypoint.t -> t
+
+
(** Add waypoints *)
+
val add_waypoints : t -> Waypoint.t list -> t
+
+
(** Add route *)
+
val add_route : t -> Route.t -> t
+
+
(** Add routes *)
+
val add_routes : t -> Route.t list -> t
+
+
(** Add track *)
+
val add_track : t -> Track.t -> t
+
+
(** Add tracks *)
+
val add_tracks : t -> Track.t list -> t
+
+
(** Add extensions *)
+
val add_extensions : t -> Extension.t list -> t
+
+
(** Clear waypoints *)
+
val clear_waypoints : t -> t
+
+
(** Clear routes *)
+
val clear_routes : t -> t
+
+
(** Clear tracks *)
+
val clear_tracks : t -> t
+
+
(** {2 Document Analysis} *)
+
+
(** Count waypoints *)
+
val waypoint_count : t -> int
+
+
(** Count routes *)
+
val route_count : t -> int
+
+
(** Count tracks *)
+
val track_count : t -> int
+
+
(** Count total points *)
+
val total_points : t -> int
+
+
(** Check if document has elevation data *)
+
val has_elevation : t -> bool
+
+
(** Check if document has time data *)
+
val has_time : t -> bool
+
+
(** Check if document is empty *)
+
val is_empty : t -> bool
+
+
(** Get document statistics *)
+
val stats : t -> stats
+
+
(** Pretty print statistics *)
+
val pp_stats : Format.formatter -> t -> unit
+
+
(** {2 Comparison and Utilities} *)
+
+
(** Compare documents *)
+
val compare : t -> t -> int
+
+
(** Test document equality *)
+
val equal : t -> t -> bool
+
+
(** Pretty print document *)
+
val pp : Format.formatter -> t -> unit
+14 -1
lib/gpx/dune
···
(public_name mlgpx.core)
(name gpx)
(libraries xmlm ptime)
-
(modules gpx types parser writer validate))
+
(modules
+
gpx
+
parser
+
writer
+
validate
+
coordinate
+
link
+
extension
+
waypoint
+
metadata
+
route
+
track
+
error
+
doc))
+111
lib/gpx/error.ml
···
+
(** Error types and exception handling for GPX operations *)
+
+
(** Main error type *)
+
type t =
+
| Invalid_xml of string
+
| Invalid_coordinate of string
+
| Missing_required_attribute of string * string
+
| Missing_required_element of string
+
| Validation_error of string
+
| Xml_error of string
+
| IO_error of string
+
+
(** GPX exception *)
+
exception Gpx_error of t
+
+
(** Result type for operations that can fail *)
+
type 'a result = ('a, t) Result.t
+
+
(** {2 Error Operations} *)
+
+
(** Convert error to string *)
+
let to_string = function
+
| Invalid_xml msg -> "Invalid XML: " ^ msg
+
| Invalid_coordinate msg -> "Invalid coordinate: " ^ msg
+
| Missing_required_attribute (element, attr) ->
+
Printf.sprintf "Missing required attribute '%s' in element '%s'" attr element
+
| Missing_required_element element ->
+
Printf.sprintf "Missing required element '%s'" element
+
| Validation_error msg -> "Validation error: " ^ msg
+
| Xml_error msg -> "XML error: " ^ msg
+
| IO_error msg -> "IO error: " ^ msg
+
+
(** Pretty print error *)
+
let pp ppf error = Format.fprintf ppf "%s" (to_string error)
+
+
(** Create invalid XML error *)
+
let invalid_xml msg = Invalid_xml msg
+
+
(** Create invalid coordinate error *)
+
let invalid_coordinate msg = Invalid_coordinate msg
+
+
(** Create missing attribute error *)
+
let missing_attribute element attr = Missing_required_attribute (element, attr)
+
+
(** Create missing element error *)
+
let missing_element element = Missing_required_element element
+
+
(** Create validation error *)
+
let validation_error msg = Validation_error msg
+
+
(** Create XML error *)
+
let xml_error msg = Xml_error msg
+
+
(** Create IO error *)
+
let io_error msg = IO_error msg
+
+
(** Compare errors *)
+
let compare e1 e2 = String.compare (to_string e1) (to_string e2)
+
+
(** Test error equality *)
+
let equal e1 e2 = compare e1 e2 = 0
+
+
(** {2 Result Helpers} *)
+
+
(** Convert exception to result *)
+
let catch f x =
+
try Ok (f x)
+
with Gpx_error e -> Error e
+
+
(** Convert result to exception *)
+
let get_exn = function
+
| Ok x -> x
+
| Error e -> raise (Gpx_error e)
+
+
(** Map over result *)
+
let map f = function
+
| Ok x -> Ok (f x)
+
| Error e -> Error e
+
+
(** Bind over result *)
+
let bind result f =
+
match result with
+
| Ok x -> f x
+
| Error e -> Error e
+
+
(** Convert string result to error result *)
+
let from_string_result = function
+
| Ok x -> Ok x
+
| Error msg -> Error (Invalid_xml msg)
+
+
(** {2 Error Classification} *)
+
+
(** Check if error is XML-related *)
+
let is_xml_error = function
+
| Invalid_xml _ | Xml_error _ -> true
+
| _ -> false
+
+
(** Check if error is coordinate-related *)
+
let is_coordinate_error = function
+
| Invalid_coordinate _ -> true
+
| _ -> false
+
+
(** Check if error is validation-related *)
+
let is_validation_error = function
+
| Validation_error _ | Missing_required_attribute _ | Missing_required_element _ -> true
+
| _ -> false
+
+
(** Check if error is IO-related *)
+
let is_io_error = function
+
| IO_error _ -> true
+
| _ -> false
+85
lib/gpx/error.mli
···
+
(** Error types and exception handling for GPX operations *)
+
+
(** Main error type *)
+
type t =
+
| Invalid_xml of string (** XML parsing/structure error *)
+
| Invalid_coordinate of string (** Coordinate validation error *)
+
| Missing_required_attribute of string * string (** Missing XML attribute (element, attr) *)
+
| Missing_required_element of string (** Missing XML element *)
+
| Validation_error of string (** GPX validation error *)
+
| Xml_error of string (** Lower-level XML error *)
+
| IO_error of string (** File I/O error *)
+
+
(** GPX exception *)
+
exception Gpx_error of t
+
+
(** Result type for operations that can fail *)
+
type 'a result = ('a, t) Result.t
+
+
(** {2 Error Operations} *)
+
+
(** Convert error to human-readable string *)
+
val to_string : t -> string
+
+
(** Pretty print error *)
+
val pp : Format.formatter -> t -> unit
+
+
(** Compare errors *)
+
val compare : t -> t -> int
+
+
(** Test error equality *)
+
val equal : t -> t -> bool
+
+
(** {2 Error Constructors} *)
+
+
(** Create invalid XML error *)
+
val invalid_xml : string -> t
+
+
(** Create invalid coordinate error *)
+
val invalid_coordinate : string -> t
+
+
(** Create missing attribute error *)
+
val missing_attribute : string -> string -> t
+
+
(** Create missing element error *)
+
val missing_element : string -> t
+
+
(** Create validation error *)
+
val validation_error : string -> t
+
+
(** Create XML error *)
+
val xml_error : string -> t
+
+
(** Create IO error *)
+
val io_error : string -> t
+
+
(** {2 Result Helpers} *)
+
+
(** Convert exception to result *)
+
val catch : ('a -> 'b) -> 'a -> 'b result
+
+
(** Convert result to exception *)
+
val get_exn : 'a result -> 'a
+
+
(** Map over result *)
+
val map : ('a -> 'b) -> 'a result -> 'b result
+
+
(** Bind over result *)
+
val bind : 'a result -> ('a -> 'b result) -> 'b result
+
+
(** Convert string result to error result *)
+
val from_string_result : ('a, string) Result.t -> 'a result
+
+
(** {2 Error Classification} *)
+
+
(** Check if error is XML-related *)
+
val is_xml_error : t -> bool
+
+
(** Check if error is coordinate-related *)
+
val is_coordinate_error : t -> bool
+
+
(** Check if error is validation-related *)
+
val is_validation_error : t -> bool
+
+
(** Check if error is IO-related *)
+
val is_io_error : t -> bool
+144
lib/gpx/extension.ml
···
+
(** Extension mechanism for custom GPX elements *)
+
+
(** Main extension type *)
+
type t = {
+
namespace : string option;
+
name : string;
+
attributes : (string * string) list;
+
content : content;
+
}
+
+
(** Content types for extensions *)
+
and content =
+
| Text of string
+
| Elements of t list
+
| Mixed of string * t list
+
+
(** {2 Extension Operations} *)
+
+
(** Create extension with flexible content *)
+
let make ?namespace ~name ~attributes ~content () =
+
{ namespace; name; attributes; content }
+
+
(** Create an extension with text content *)
+
let make_text ~name ?namespace ?(attributes=[]) text =
+
{ namespace; name; attributes; content = Text text }
+
+
(** Create an extension with element content *)
+
let make_elements ~name ?namespace ?(attributes=[]) elements =
+
{ namespace; name; attributes; content = Elements elements }
+
+
(** Create an extension with mixed content *)
+
let make_mixed ~name ?namespace ?(attributes=[]) text elements =
+
{ namespace; name; attributes; content = Mixed (text, elements) }
+
+
(** Get extension name *)
+
let name t = t.name
+
+
(** Get optional namespace *)
+
let namespace t = t.namespace
+
+
(** Get attributes *)
+
let attributes t = t.attributes
+
+
(** Get content *)
+
let content t = t.content
+
+
(** Create text content *)
+
let text_content text = Text text
+
+
(** Create elements content *)
+
let elements_content elements = Elements elements
+
+
(** Create mixed content *)
+
let mixed_content text elements = Mixed (text, elements)
+
+
(** Find attribute value by name *)
+
let find_attribute name t =
+
List.assoc_opt name t.attributes
+
+
(** Add or update attribute *)
+
let set_attribute name value t =
+
let attributes =
+
(name, value) :: List.remove_assoc name t.attributes
+
in
+
{ t with attributes }
+
+
(** Compare extensions *)
+
let rec compare t1 t2 =
+
let ns_cmp = Option.compare String.compare t1.namespace t2.namespace in
+
if ns_cmp <> 0 then ns_cmp
+
else
+
let name_cmp = String.compare t1.name t2.name in
+
if name_cmp <> 0 then name_cmp
+
else
+
let attr_cmp = compare_attributes t1.attributes t2.attributes in
+
if attr_cmp <> 0 then attr_cmp
+
else compare_content t1.content t2.content
+
+
and compare_attributes attrs1 attrs2 =
+
let sorted1 = List.sort (fun (k1,_) (k2,_) -> String.compare k1 k2) attrs1 in
+
let sorted2 = List.sort (fun (k1,_) (k2,_) -> String.compare k1 k2) attrs2 in
+
List.compare (fun (k1,v1) (k2,v2) ->
+
let k_cmp = String.compare k1 k2 in
+
if k_cmp <> 0 then k_cmp else String.compare v1 v2
+
) sorted1 sorted2
+
+
and compare_content c1 c2 = match c1, c2 with
+
| Text s1, Text s2 -> String.compare s1 s2
+
| Elements e1, Elements e2 -> List.compare compare e1 e2
+
| Mixed (s1, e1), Mixed (s2, e2) ->
+
let s_cmp = String.compare s1 s2 in
+
if s_cmp <> 0 then s_cmp else List.compare compare e1 e2
+
| Text _, _ -> -1
+
| Elements _, Text _ -> 1
+
| Elements _, Mixed _ -> -1
+
| Mixed _, _ -> 1
+
+
(** Test extension equality *)
+
let equal t1 t2 = compare t1 t2 = 0
+
+
(** Pretty print extension *)
+
let rec pp ppf t =
+
match t.namespace with
+
| Some ns -> Format.fprintf ppf "<%s:%s" ns t.name
+
| None -> Format.fprintf ppf "<%s" t.name;
+
List.iter (fun (k, v) -> Format.fprintf ppf " %s=\"%s\"" k v) t.attributes;
+
match t.content with
+
| Text "" -> Format.fprintf ppf "/>"
+
| Text text -> Format.fprintf ppf ">%s</%s>" text (qualified_name t)
+
| Elements [] -> Format.fprintf ppf "/>"
+
| Elements elements ->
+
Format.fprintf ppf ">";
+
List.iter (Format.fprintf ppf "%a" pp) elements;
+
Format.fprintf ppf "</%s>" (qualified_name t)
+
| Mixed (text, []) -> Format.fprintf ppf ">%s</%s>" text (qualified_name t)
+
| Mixed (text, elements) ->
+
Format.fprintf ppf ">%s" text;
+
List.iter (Format.fprintf ppf "%a" pp) elements;
+
Format.fprintf ppf "</%s>" (qualified_name t)
+
+
and qualified_name t =
+
match t.namespace with
+
| Some ns -> ns ^ ":" ^ t.name
+
| None -> t.name
+
+
(** {2 Content Operations} *)
+
+
(** Check if content is text *)
+
let is_text_content = function Text _ -> true | _ -> false
+
+
(** Check if content is elements *)
+
let is_elements_content = function Elements _ -> true | _ -> false
+
+
(** Check if content is mixed *)
+
let is_mixed_content = function Mixed _ -> true | _ -> false
+
+
(** Extract text content *)
+
let text_content_extract = function Text s -> Some s | _ -> None
+
+
(** Extract element content *)
+
let elements_content_extract = function Elements e -> Some e | _ -> None
+
+
(** Extract mixed content *)
+
let mixed_content_extract = function Mixed (s, e) -> Some (s, e) | _ -> None
+87
lib/gpx/extension.mli
···
+
(** Extension mechanism for custom GPX elements *)
+
+
(** Main extension type *)
+
type t = {
+
namespace : string option; (** Optional XML namespace *)
+
name : string; (** Element name *)
+
attributes : (string * string) list; (** Element attributes *)
+
content : content; (** Element content *)
+
}
+
+
(** Content types for extensions *)
+
and content =
+
| Text of string (** Simple text content *)
+
| Elements of t list (** Nested elements *)
+
| Mixed of string * t list (** Mixed text and elements *)
+
+
(** {2 Extension Constructors} *)
+
+
(** Create extension with flexible content *)
+
val make : ?namespace:string -> name:string -> attributes:(string * string) list -> content:content -> unit -> t
+
+
(** Create an extension with text content *)
+
val make_text : name:string -> ?namespace:string -> ?attributes:(string * string) list -> string -> t
+
+
(** Create an extension with element content *)
+
val make_elements : name:string -> ?namespace:string -> ?attributes:(string * string) list -> t list -> t
+
+
(** Create an extension with mixed content *)
+
val make_mixed : name:string -> ?namespace:string -> ?attributes:(string * string) list -> string -> t list -> t
+
+
(** {2 Extension Operations} *)
+
+
(** Get extension name *)
+
val name : t -> string
+
+
(** Get optional namespace *)
+
val namespace : t -> string option
+
+
(** Get attributes *)
+
val attributes : t -> (string * string) list
+
+
(** Get content *)
+
val content : t -> content
+
+
(** Find attribute value by name *)
+
val find_attribute : string -> t -> string option
+
+
(** Add or update attribute *)
+
val set_attribute : string -> string -> t -> t
+
+
(** Compare extensions *)
+
val compare : t -> t -> int
+
+
(** Test extension equality *)
+
val equal : t -> t -> bool
+
+
(** Pretty print extension *)
+
val pp : Format.formatter -> t -> unit
+
+
(** {2 Content Operations} *)
+
+
(** Create text content *)
+
val text_content : string -> content
+
+
(** Create elements content *)
+
val elements_content : t list -> content
+
+
(** Create mixed content *)
+
val mixed_content : string -> t list -> content
+
+
(** Check if content is text *)
+
val is_text_content : content -> bool
+
+
(** Check if content is elements *)
+
val is_elements_content : content -> bool
+
+
(** Check if content is mixed *)
+
val is_mixed_content : content -> bool
+
+
(** Extract text content *)
+
val text_content_extract : content -> string option
+
+
(** Extract element content *)
+
val elements_content_extract : content -> t list option
+
+
(** Extract mixed content *)
+
val mixed_content_extract : content -> (string * t list) option
+86 -56
lib/gpx/gpx.ml
···
-
(** {1 mlgpx - OCaml GPX Library} *)
+
(** OCaml library for reading and writing GPX (GPS Exchange Format) files *)
-
(** Core type definitions and utilities *)
-
module Types = Types
+
(** {1 Core Modules} *)
-
(** Streaming XML parser *)
-
module Parser = Parser
+
(** Geographic coordinate handling *)
+
module Coordinate = Coordinate
-
(** Streaming XML writer *)
-
module Writer = Writer
+
(** Links, persons, and copyright information *)
+
module Link = Link
-
(** Validation engine *)
-
module Validate = Validate
+
(** Extension mechanism for custom GPX elements *)
+
module Extension = Extension
-
(* Re-export core types for direct access *)
-
type latitude = Types.latitude
-
type longitude = Types.longitude
-
type degrees = Types.degrees
-
type fix_type = Types.fix_type = None_fix | Fix_2d | Fix_3d | Dgps | Pps
-
type person = Types.person = { name : string option; email : string option; link : link option }
-
and link = Types.link = { href : string; text : string option; type_ : string option }
-
type copyright = Types.copyright = { author : string; year : int option; license : string option }
-
type bounds = Types.bounds = { minlat : latitude; minlon : longitude; maxlat : latitude; maxlon : longitude }
-
type extension_content = Types.extension_content = Text of string | Elements of extension list | Mixed of string * extension list
-
and extension = Types.extension = { namespace : string option; name : string; attributes : (string * string) list; content : extension_content }
-
type metadata = Types.metadata = { name : string option; desc : string option; author : person option; copyright : copyright option; links : link list; time : Ptime.t option; keywords : string option; bounds : bounds option; extensions : extension list }
-
type waypoint_data = Types.waypoint_data = { lat : latitude; lon : longitude; ele : float option; time : Ptime.t option; magvar : degrees option; geoidheight : float option; name : string option; cmt : string option; desc : string option; src : string option; links : link list; sym : string option; type_ : string option; fix : fix_type option; sat : int option; hdop : float option; vdop : float option; pdop : float option; ageofdgpsdata : float option; dgpsid : int option; extensions : extension list }
-
type waypoint = Types.waypoint
-
type route_point = Types.route_point
-
type track_point = Types.track_point
-
type route = Types.route = { name : string option; cmt : string option; desc : string option; src : string option; links : link list; number : int option; type_ : string option; extensions : extension list; rtepts : route_point list }
-
type track_segment = Types.track_segment = { trkpts : track_point list; extensions : extension list }
-
type track = Types.track = { name : string option; cmt : string option; desc : string option; src : string option; links : link list; number : int option; type_ : string option; extensions : extension list; trksegs : track_segment list }
-
type gpx = Types.gpx = { version : string; creator : string; metadata : metadata option; waypoints : waypoint list; routes : route list; tracks : track list; extensions : extension list }
-
type error = Types.error = Invalid_xml of string | Invalid_coordinate of string | Missing_required_attribute of string * string | Missing_required_element of string | Validation_error of string | Xml_error of string | IO_error of string
-
exception Gpx_error = Types.Gpx_error
-
type 'a result = ('a, error) Result.t
-
type validation_issue = Validate.validation_issue = { level : [`Error | `Warning]; message : string; location : string option }
-
type validation_result = Validate.validation_result = { issues : validation_issue list; is_valid : bool }
+
(** GPS waypoint data and fix types *)
+
module Waypoint = Waypoint
-
(* Re-export core functions *)
-
let latitude = Types.latitude
-
let longitude = Types.longitude
-
let degrees = Types.degrees
-
let latitude_to_float = Types.latitude_to_float
-
let longitude_to_float = Types.longitude_to_float
-
let degrees_to_float = Types.degrees_to_float
-
let fix_type_to_string = Types.fix_type_to_string
-
let fix_type_of_string = Types.fix_type_of_string
-
let make_waypoint_data = Types.make_waypoint_data
-
let empty_metadata = Types.empty_metadata
-
let make_gpx = Types.make_gpx
+
(** GPX metadata including bounds *)
+
module Metadata = Metadata
-
(* Re-export parser functions *)
-
let parse = Parser.parse
-
let parse_string = Parser.parse_string
+
(** Route data and calculations *)
+
module Route = Route
-
(* Re-export writer functions *)
-
let write = Writer.write
-
let write_string = Writer.write_string
+
(** Track data with segments *)
+
module Track = Track
-
(* Re-export validation functions *)
+
(** Error handling *)
+
module Error = Error
+
+
(** Main GPX document type *)
+
module Doc = Doc
+
+
(** {1 Main Document Type} *)
+
+
(** Main GPX document type *)
+
type t = Doc.t
+
+
(** {1 Error Handling} *)
+
+
(** Error types *)
+
type error = Error.t
+
+
(** GPX exception *)
+
exception Gpx_error of error
+
+
(** {1 Parsing Functions} *)
+
+
(** Parse GPX from XML input *)
+
let parse ?validate input = Parser.parse ?validate input
+
+
(** Parse GPX from string *)
+
let parse_string ?validate s = Parser.parse_string ?validate s
+
+
(** {1 Writing Functions} *)
+
+
(** Write GPX to XML output *)
+
let write ?validate output gpx = Writer.write ?validate output gpx
+
+
(** Write GPX to string *)
+
let write_string ?validate gpx = Writer.write_string ?validate gpx
+
+
(** {1 Validation Functions} *)
+
+
(** Validation issue with severity level *)
+
type validation_issue = Validate.validation_issue = {
+
level : [`Error | `Warning];
+
message : string;
+
location : string option;
+
}
+
+
(** Result of validation containing all issues found *)
+
type validation_result = Validate.validation_result = {
+
issues : validation_issue list;
+
is_valid : bool;
+
}
+
+
(** Validate complete GPX document *)
let validate_gpx = Validate.validate_gpx
+
+
(** Quick validation - returns true if document is valid *)
let is_valid = Validate.is_valid
-
let get_errors = Validate.get_errors
-
let get_warnings = Validate.get_warnings
-
let format_issue = Validate.format_issue
+
+
(** Get only error messages *)
+
let errors = Validate.errors
+
+
(** Get only warning messages *)
+
let warnings = Validate.warnings
+
+
(** Format validation issue for display *)
+
let format_issue = Validate.format_issue
+
+
(** {1 Constructors and Utilities} *)
+
+
(** Create new GPX document *)
+
let make_gpx ~creator = Doc.empty ~creator
+
+
(** Create empty GPX document *)
+
let empty ~creator = Doc.empty ~creator
+462 -317
lib/gpx/gpx.mli
···
-
(** {1 MLGpx - OCaml GPX Library}
-
-
A high-quality OCaml library for parsing and generating GPX (GPS Exchange Format) files.
-
GPX is a standardized XML format for exchanging GPS data between applications and devices.
-
-
{2 Overview}
-
-
The GPX format defines a standard way to describe waypoints, routes, and tracks.
-
This library provides a complete implementation of GPX 1.1 with strong type safety
-
and memory-efficient streaming processing.
-
-
{b Key Features:}
-
- ✅ Complete GPX 1.1 support with all standard elements
-
- ✅ Type-safe coordinate validation (WGS84 datum)
-
- ✅ Memory-efficient streaming parser and writer
-
- ✅ Comprehensive validation with detailed error reporting
-
- ✅ Extension support for custom elements
-
- ✅ Cross-platform (core has no Unix dependencies)
-
-
{2 Quick Start}
-
+
(** 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 = latitude 37.7749 in
-
let* lon = longitude (-122.4194) in
+
let lat = Coordinate.latitude 37.7749 |> Result.get_ok in
+
let lon = Coordinate.longitude (-122.4194) |> Result.get_ok in
(* Create a waypoint *)
-
let wpt = make_waypoint_data lat lon in
-
let wpt = { wpt with name = Some "San Francisco" } in
+
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:"mlgpx" in
-
let gpx = { gpx with waypoints = [wpt] } in
+
let gpx = make_gpx ~creator:"my-app"
+
|> Doc.add_waypoint waypoint in
-
(* Convert to XML string *)
-
write_string gpx
+
(* 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)
]}
-
-
{2 Core Types} *)
-
-
(** {3 Geographic Coordinates}
-
-
All coordinates use the WGS84 datum as specified by the GPX standard. *)
-
-
(** Latitude coordinate (-90.0 to 90.0 degrees).
-
Private type ensures validation through smart constructor. *)
-
type latitude = Types.latitude
-
-
(** Longitude coordinate (-180.0 to 180.0 degrees).
-
Private type ensures validation through smart constructor. *)
-
type longitude = Types.longitude
-
-
(** Degrees for magnetic variation (0.0 to 360.0 degrees).
-
Private type ensures validation through smart constructor. *)
-
type degrees = Types.degrees
-
-
(** Create validated latitude coordinate.
-
@param lat Latitude in degrees (-90.0 to 90.0)
-
@return [Ok lat] if valid, [Error msg] if out of range *)
-
val latitude : float -> (latitude, string) result
-
-
(** Create validated longitude coordinate.
-
@param lon Longitude in degrees (-180.0 to 180.0)
-
@return [Ok lon] if valid, [Error msg] if out of range *)
-
val longitude : float -> (longitude, string) result
-
-
(** Create validated degrees value.
-
@param deg Degrees (0.0 to 360.0)
-
@return [Ok deg] if valid, [Error msg] if out of range *)
-
val degrees : float -> (degrees, string) result
-
-
(** Convert latitude back to float *)
-
val latitude_to_float : latitude -> float
-
-
(** Convert longitude back to float *)
-
val longitude_to_float : longitude -> float
-
-
(** Convert degrees back to float *)
-
val degrees_to_float : degrees -> float
-
-
(** {3 GPS Fix Types}
-
-
Standard GPS fix types as defined in the GPX specification. *)
-
-
(** GPS fix type indicating the quality/type of GPS reading *)
-
type fix_type = Types.fix_type =
-
| None_fix (** No fix available *)
-
| Fix_2d (** 2D fix (latitude/longitude) *)
-
| Fix_3d (** 3D fix (latitude/longitude/altitude) *)
-
| Dgps (** Differential GPS *)
-
| Pps (** Precise Positioning Service *)
-
-
(** Convert fix type to string representation *)
-
val fix_type_to_string : fix_type -> string
-
-
(** Parse fix type from string *)
-
val fix_type_of_string : string -> fix_type option
-
-
(** {3 Metadata Elements} *)
-
-
(** Person information for author, copyright holder, etc. *)
-
type person = Types.person = {
-
name : string option; (** Person's name *)
-
email : string option; (** Email address *)
-
link : link option; (** Link to person's website *)
-
}
-
-
(** External link with optional description and type *)
-
and link = Types.link = {
-
href : string; (** URL of the link *)
-
text : string option; (** Text description of link *)
-
type_ : string option; (** MIME type of linked content *)
-
}
-
-
(** Copyright information for the GPX file *)
-
type copyright = Types.copyright = {
-
author : string; (** Copyright holder *)
-
year : int option; (** Year of copyright *)
-
license : string option; (** License terms *)
-
}
-
-
(** Geographic bounds - minimum bounding rectangle *)
-
type bounds = Types.bounds = {
-
minlat : latitude; (** Minimum latitude *)
-
minlon : longitude; (** Minimum longitude *)
-
maxlat : latitude; (** Maximum latitude *)
-
maxlon : longitude; (** Maximum longitude *)
-
}
-
-
(** Extension content for custom elements *)
-
type extension_content = Types.extension_content =
-
| Text of string (** Text content *)
-
| Elements of extension list (** Child elements *)
-
| Mixed of string * extension list (** Mixed text and elements *)
-
-
(** Extension element for custom data *)
-
and extension = Types.extension = {
-
namespace : string option; (** XML namespace *)
-
name : string; (** Element name *)
-
attributes : (string * string) list; (** Element attributes *)
-
content : extension_content; (** Element content *)
-
}
-
-
(** GPX file metadata containing information about the file itself *)
-
type metadata = Types.metadata = {
-
name : string option; (** Name of GPX file *)
-
desc : string option; (** Description of contents *)
-
author : person option; (** Person who created GPX file *)
-
copyright : copyright option; (** Copyright information *)
-
links : link list; (** Related links *)
-
time : Ptime.t option; (** Creation/modification time *)
-
keywords : string option; (** Keywords for searching *)
-
bounds : bounds option; (** Geographic bounds *)
-
extensions : extension list; (** Custom extensions *)
-
}
-
-
(** Create empty metadata record *)
-
val empty_metadata : metadata
-
-
(** {3 Geographic Points}
-
-
All geographic points (waypoints, route points, track points) share the same structure. *)
-
-
(** Base waypoint data structure used for all geographic points.
-
Contains position, time, and various GPS-related fields. *)
-
type waypoint_data = Types.waypoint_data = {
-
lat : latitude; (** Latitude coordinate *)
-
lon : longitude; (** Longitude coordinate *)
-
ele : float option; (** Elevation in meters *)
-
time : Ptime.t option; (** Time of GPS reading *)
-
magvar : degrees option; (** Magnetic variation at point *)
-
geoidheight : float option; (** Height of geoid above WGS84 ellipsoid *)
-
name : string option; (** Point name *)
-
cmt : string option; (** GPS comment *)
-
desc : string option; (** Point description *)
-
src : string option; (** Source of data *)
-
links : link list; (** Related links *)
-
sym : string option; (** GPS symbol name *)
-
type_ : string option; (** Point classification *)
-
fix : fix_type option; (** Type of GPS fix *)
-
sat : int option; (** Number of satellites *)
-
hdop : float option; (** Horizontal dilution of precision *)
-
vdop : float option; (** Vertical dilution of precision *)
-
pdop : float option; (** Position dilution of precision *)
-
ageofdgpsdata : float option; (** Age of DGPS data *)
-
dgpsid : int option; (** DGPS station ID *)
-
extensions : extension list; (** Custom extensions *)
-
}
+
+
This library provides a clean, modular interface for working with GPX files,
+
with separate modules for each major component of the GPX specification. *)
-
(** Create basic waypoint data with required coordinates *)
-
val make_waypoint_data : latitude -> longitude -> waypoint_data
+
(** {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. *)
-
(** Individual waypoint - a point of interest *)
-
type waypoint = Types.waypoint
+
(** {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
-
(** Route point - point along a planned route *)
-
type route_point = Types.route_point
+
(** {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
-
(** Track point - recorded position along an actual path *)
-
type track_point = Types.track_point
+
(** {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
-
(** {3 Routes}
+
(** {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
-
A route is an ordered list of waypoints representing a planned path. *)
+
(** {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
-
(** Route definition - ordered list of waypoints for navigation *)
-
type route = Types.route = {
-
name : string option; (** Route name *)
-
cmt : string option; (** GPS comment *)
-
desc : string option; (** Route description *)
-
src : string option; (** Source of data *)
-
links : link list; (** Related links *)
-
number : int option; (** Route number *)
-
type_ : string option; (** Route classification *)
-
extensions : extension list; (** Custom extensions *)
-
rtepts : route_point list; (** Route points *)
-
}
+
(** {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
-
(** {3 Tracks}
+
(** {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
-
A track represents an actual recorded path, consisting of track segments. *)
+
(** {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
-
(** Track segment - continuous set of track points *)
-
type track_segment = Types.track_segment = {
-
trkpts : track_point list; (** Track points in segment *)
-
extensions : extension list; (** Custom extensions *)
-
}
+
(** {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
-
(** Track definition - recorded path made up of segments *)
-
type track = Types.track = {
-
name : string option; (** Track name *)
-
cmt : string option; (** GPS comment *)
-
desc : string option; (** Track description *)
-
src : string option; (** Source of data *)
-
links : link list; (** Related links *)
-
number : int option; (** Track number *)
-
type_ : string option; (** Track classification *)
-
extensions : extension list; (** Custom extensions *)
-
trksegs : track_segment list; (** Track segments *)
-
}
+
(** {1 Main Document Type} *)
-
(** {3 Main GPX Document}
-
-
The root GPX element contains metadata and collections of waypoints, routes, and tracks. *)
-
-
(** Main GPX document conforming to GPX 1.1 standard *)
-
type gpx = Types.gpx = {
-
version : string; (** GPX version (always "1.1") *)
-
creator : string; (** Creating application *)
-
metadata : metadata option; (** File metadata *)
-
waypoints : waypoint list; (** Waypoints *)
-
routes : route list; (** Routes *)
-
tracks : track list; (** Tracks *)
-
extensions : extension list; (** Custom extensions *)
-
}
-
-
(** Create GPX document with required creator field *)
-
val make_gpx : creator:string -> gpx
+
(** 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
-
(** {3 Error Handling} *)
+
(** {1 Error Handling} *)
-
(** Errors that can occur during GPX processing *)
-
type error = Types.error =
-
| Invalid_xml of string (** XML parsing error *)
-
| Invalid_coordinate of string (** Coordinate validation error *)
-
| Missing_required_attribute of string * string (** Missing XML attribute *)
-
| Missing_required_element of string (** Missing XML element *)
-
| Validation_error of string (** GPX validation error *)
-
| Xml_error of string (** XML processing error *)
-
| IO_error of string (** I/O error *)
+
(** 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
-
(** Exception type for GPX errors *)
+
(** 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
-
(** Result type for operations that may fail *)
-
type 'a result = ('a, error) Result.t
+
(** {1 Parsing Functions}
+
+
Parse GPX data from various sources. All parsing functions support optional
+
validation to check compliance with GPX specification constraints. *)
-
(** {2 Parsing Functions}
+
(** 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 documents from XML input sources. *)
+
(** 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 = {|<?xml version="1.0"?>
+
<gpx version="1.1" creator="my-app">
+
<wpt lat="37.7749" lon="-122.4194">
+
<name>San Francisco</name>
+
</wpt>
+
</gpx>|} 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
-
(** Parse GPX document from xmlm input source.
-
@param input The xmlm input source
-
@return [Ok gpx] on success, [Error err] on failure *)
-
val parse : Xmlm.input -> gpx result
+
(** {1 Writing Functions}
+
+
Generate GPX XML from document structures. All writing functions support
+
optional validation before output generation. *)
-
(** Parse GPX document from string.
-
@param xml_string GPX document as XML string
-
@return [Ok gpx] on success, [Error err] on failure *)
-
val parse_string : string -> gpx result
+
(** 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
-
(** {2 Writing Functions}
+
(** 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
-
Generate GPX XML from document structures. *)
+
(** {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. *)
-
(** Write GPX document to xmlm output destination.
-
@param output The xmlm output destination
-
@param gpx The GPX document to write
-
@return [Ok ()] on success, [Error err] on failure *)
-
val write : Xmlm.output -> gpx -> unit result
-
-
(** Write GPX document to XML string.
-
@param gpx The GPX document to write
-
@return [Ok xml_string] on success, [Error err] on failure *)
-
val write_string : gpx -> string result
-
-
(** {2 Validation Functions}
-
-
Validate GPX documents for correctness and best practices. *)
-
-
(** Validation issue with severity level *)
+
(** 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]; (** Severity level *)
-
message : string; (** Issue description *)
-
location : string option; (** Location in document *)
+
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") *)
}
-
(** Result of validation containing all issues found *)
+
(** 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 *)
-
is_valid : bool; (** True if no errors found *)
+
issues : validation_issue list; (** All validation issues found, both errors and warnings *)
+
is_valid : bool; (** [true] if no errors found (warnings are allowed) *)
}
-
(** Validate complete GPX document.
-
Checks coordinates, required fields, and best practices.
-
@param gpx GPX document to validate
-
@return Validation result with any issues found *)
-
val validate_gpx : gpx -> validation_result
+
(** 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.
-
@param gpx GPX document to validate
-
@return [true] if document is valid (no errors) *)
-
val is_valid : gpx -> bool
+
(** 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 error-level validation issues.
-
@param gpx GPX document to validate
-
@return List of validation errors *)
-
val get_errors : gpx -> validation_issue list
+
(** 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 warning-level validation issues.
-
@param gpx GPX document to validate
-
@return List of validation warnings *)
-
val get_warnings : gpx -> 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 validation issue for display.
-
@param issue Validation issue to format
-
@return Human-readable error message *)
+
(** 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
-
(** {2 Module Access}
+
(** {1 Document Constructors and Utilities}
+
+
Functions for creating GPX documents and basic document operations. *)
-
Direct access to submodules for advanced usage. *)
-
-
(** Core type definitions and utilities *)
-
module Types = Types
+
(** 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
-
(** Streaming XML parser *)
-
module Parser = Parser
+
(** 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
-
(** Streaming XML writer *)
-
module Writer = Writer
+
(** {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]
+
]} *)
-
(** Validation engine *)
-
module Validate = Validate
+
(** {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} *)
+126
lib/gpx/link.ml
···
+
(** Link and person information types *)
+
+
(** Main link type *)
+
type t = {
+
href : string;
+
text : string option;
+
type_ : string option;
+
}
+
+
(** Person information *)
+
and person = {
+
name : string option;
+
email : string option;
+
link : t option;
+
}
+
+
(** Copyright information *)
+
and copyright = {
+
author : string;
+
year : int option;
+
license : string option;
+
}
+
+
(** {2 Link Operations} *)
+
+
(** Create a link *)
+
let make ~href ?text ?type_ () = { href; text; type_ }
+
+
(** Get href from link *)
+
let href t = t.href
+
+
(** Get optional text from link *)
+
let text t = t.text
+
+
(** Get optional type from link *)
+
let type_ t = t.type_
+
+
(** Update text *)
+
let with_text t text = { t with text = Some text }
+
+
(** Update type *)
+
let with_type t type_ = { t with type_ = Some type_ }
+
+
(** Compare links *)
+
let compare t1 t2 =
+
let href_cmp = String.compare t1.href t2.href in
+
if href_cmp <> 0 then href_cmp
+
else
+
let text_cmp = Option.compare String.compare t1.text t2.text in
+
if text_cmp <> 0 then text_cmp
+
else Option.compare String.compare t1.type_ t2.type_
+
+
(** Test link equality *)
+
let equal t1 t2 = compare t1 t2 = 0
+
+
(** Pretty print link *)
+
let pp ppf t =
+
match t.text with
+
| Some text -> Format.fprintf ppf "%s (%s)" text t.href
+
| None -> Format.fprintf ppf "%s" t.href
+
+
(** {2 Person Operations} *)
+
+
(** Create person *)
+
let make_person ?name ?email ?link () = { name; email; link }
+
+
(** Get person name *)
+
let person_name (p : person) = p.name
+
+
(** Get person email *)
+
let person_email (p : person) = p.email
+
+
(** Get person link *)
+
let person_link (p : person) = p.link
+
+
(** Compare persons *)
+
let compare_person p1 p2 =
+
let name_cmp = Option.compare String.compare p1.name p2.name in
+
if name_cmp <> 0 then name_cmp
+
else
+
let email_cmp = Option.compare String.compare p1.email p2.email in
+
if email_cmp <> 0 then email_cmp
+
else Option.compare compare p1.link p2.link
+
+
(** Test person equality *)
+
let equal_person p1 p2 = compare_person p1 p2 = 0
+
+
(** Pretty print person *)
+
let pp_person ppf p =
+
match p.name, p.email with
+
| Some name, Some email -> Format.fprintf ppf "%s <%s>" name email
+
| Some name, None -> Format.fprintf ppf "%s" name
+
| None, Some email -> Format.fprintf ppf "<%s>" email
+
| None, None -> Format.fprintf ppf "(anonymous)"
+
+
(** {2 Copyright Operations} *)
+
+
(** Create copyright *)
+
let make_copyright ~author ?year ?license () = { author; year; license }
+
+
(** Get copyright author *)
+
let copyright_author (c : copyright) = c.author
+
+
(** Get copyright year *)
+
let copyright_year (c : copyright) = c.year
+
+
(** Get copyright license *)
+
let copyright_license (c : copyright) = c.license
+
+
(** Compare copyrights *)
+
let compare_copyright c1 c2 =
+
let author_cmp = String.compare c1.author c2.author in
+
if author_cmp <> 0 then author_cmp
+
else
+
let year_cmp = Option.compare Int.compare c1.year c2.year in
+
if year_cmp <> 0 then year_cmp
+
else Option.compare String.compare c1.license c2.license
+
+
(** Test copyright equality *)
+
let equal_copyright c1 c2 = compare_copyright c1 c2 = 0
+
+
(** Pretty print copyright *)
+
let pp_copyright ppf c =
+
match c.year with
+
| Some year -> Format.fprintf ppf "© %d %s" year c.author
+
| None -> Format.fprintf ppf "© %s" c.author
+101
lib/gpx/link.mli
···
+
(** Link and person information types *)
+
+
(** Main link type *)
+
type t = {
+
href : string;
+
text : string option;
+
type_ : string option;
+
}
+
+
(** Person information *)
+
and person = {
+
name : string option;
+
email : string option;
+
link : t option;
+
}
+
+
(** Copyright information *)
+
and copyright = {
+
author : string;
+
year : int option;
+
license : string option;
+
}
+
+
(** {2 Link Operations} *)
+
+
(** Create a link.
+
@param href URL reference (required)
+
@param ?text Optional link text
+
@param ?type_ Optional MIME type *)
+
val make : href:string -> ?text:string -> ?type_:string -> unit -> t
+
+
(** Get href from link *)
+
val href : t -> string
+
+
(** Get optional text from link *)
+
val text : t -> string option
+
+
(** Get optional type from link *)
+
val type_ : t -> string option
+
+
(** Update text *)
+
val with_text : t -> string -> t
+
+
(** Update type *)
+
val with_type : t -> string -> t
+
+
+
(** Compare links *)
+
val compare : t -> t -> int
+
+
(** Test link equality *)
+
val equal : t -> t -> bool
+
+
(** Pretty print link *)
+
val pp : Format.formatter -> t -> unit
+
+
(** {2 Person Operations} *)
+
+
(** Create person information *)
+
val make_person : ?name:string -> ?email:string -> ?link:t -> unit -> person
+
+
(** Get person name *)
+
val person_name : person -> string option
+
+
(** Get person email *)
+
val person_email : person -> string option
+
+
(** Get person link *)
+
val person_link : person -> t option
+
+
(** Compare persons *)
+
val compare_person : person -> person -> int
+
+
(** Test person equality *)
+
val equal_person : person -> person -> bool
+
+
(** Pretty print person *)
+
val pp_person : Format.formatter -> person -> unit
+
+
(** {2 Copyright Operations} *)
+
+
(** Create copyright information *)
+
val make_copyright : author:string -> ?year:int -> ?license:string -> unit -> copyright
+
+
(** Get copyright author *)
+
val copyright_author : copyright -> string
+
+
(** Get copyright year *)
+
val copyright_year : copyright -> int option
+
+
(** Get copyright license *)
+
val copyright_license : copyright -> string option
+
+
(** Compare copyrights *)
+
val compare_copyright : copyright -> copyright -> int
+
+
(** Test copyright equality *)
+
val equal_copyright : copyright -> copyright -> bool
+
+
(** Pretty print copyright *)
+
val pp_copyright : Format.formatter -> copyright -> unit
+183
lib/gpx/metadata.ml
···
+
(** GPX metadata and bounds types *)
+
+
(** Bounding box *)
+
type bounds = {
+
minlat : Coordinate.latitude;
+
minlon : Coordinate.longitude;
+
maxlat : Coordinate.latitude;
+
maxlon : Coordinate.longitude;
+
}
+
+
(** Main metadata type *)
+
type t = {
+
name : string option;
+
desc : string option;
+
author : Link.person option;
+
copyright : Link.copyright option;
+
links : Link.t list;
+
time : Ptime.t option;
+
keywords : string option;
+
bounds : bounds option;
+
extensions : Extension.t list;
+
}
+
+
(** {2 Bounds Operations} *)
+
+
module Bounds = struct
+
type t = bounds
+
+
(** Create bounds from coordinates *)
+
let make ~minlat ~minlon ~maxlat ~maxlon = { minlat; minlon; maxlat; maxlon }
+
+
(** Create bounds from float coordinates with validation *)
+
let make_from_floats ~minlat ~minlon ~maxlat ~maxlon =
+
match
+
Coordinate.latitude minlat,
+
Coordinate.longitude minlon,
+
Coordinate.latitude maxlat,
+
Coordinate.longitude maxlon
+
with
+
| Ok minlat, Ok minlon, Ok maxlat, Ok maxlon ->
+
if Coordinate.latitude_to_float minlat <= Coordinate.latitude_to_float maxlat &&
+
Coordinate.longitude_to_float minlon <= Coordinate.longitude_to_float maxlon
+
then Ok { minlat; minlon; maxlat; maxlon }
+
else Error "Invalid bounds: min values must be <= max values"
+
| Error e, _, _, _ | _, Error e, _, _ | _, _, Error e, _ | _, _, _, Error e -> Error e
+
+
(** Get corner coordinates *)
+
let min_coords t = Coordinate.make t.minlat t.minlon
+
let max_coords t = Coordinate.make t.maxlat t.maxlon
+
+
(** Get all bounds as tuple *)
+
let bounds t = (t.minlat, t.minlon, t.maxlat, t.maxlon)
+
+
(** Check if coordinate is within bounds *)
+
let contains bounds coord =
+
let lat = Coordinate.lat coord in
+
let lon = Coordinate.lon coord in
+
Coordinate.latitude_to_float bounds.minlat <= Coordinate.latitude_to_float lat &&
+
Coordinate.latitude_to_float lat <= Coordinate.latitude_to_float bounds.maxlat &&
+
Coordinate.longitude_to_float bounds.minlon <= Coordinate.longitude_to_float lon &&
+
Coordinate.longitude_to_float lon <= Coordinate.longitude_to_float bounds.maxlon
+
+
(** Calculate bounds area *)
+
let area t =
+
let lat_diff = Coordinate.latitude_to_float t.maxlat -. Coordinate.latitude_to_float t.minlat in
+
let lon_diff = Coordinate.longitude_to_float t.maxlon -. Coordinate.longitude_to_float t.minlon in
+
lat_diff *. lon_diff
+
+
(** Compare bounds *)
+
let compare t1 t2 =
+
let minlat_cmp = Float.compare
+
(Coordinate.latitude_to_float t1.minlat)
+
(Coordinate.latitude_to_float t2.minlat) in
+
if minlat_cmp <> 0 then minlat_cmp
+
else
+
let minlon_cmp = Float.compare
+
(Coordinate.longitude_to_float t1.minlon)
+
(Coordinate.longitude_to_float t2.minlon) in
+
if minlon_cmp <> 0 then minlon_cmp
+
else
+
let maxlat_cmp = Float.compare
+
(Coordinate.latitude_to_float t1.maxlat)
+
(Coordinate.latitude_to_float t2.maxlat) in
+
if maxlat_cmp <> 0 then maxlat_cmp
+
else Float.compare
+
(Coordinate.longitude_to_float t1.maxlon)
+
(Coordinate.longitude_to_float t2.maxlon)
+
+
(** Test bounds equality *)
+
let equal t1 t2 = compare t1 t2 = 0
+
+
(** Pretty print bounds *)
+
let pp ppf t =
+
Format.fprintf ppf "[(%g,%g) - (%g,%g)]"
+
(Coordinate.latitude_to_float t.minlat)
+
(Coordinate.longitude_to_float t.minlon)
+
(Coordinate.latitude_to_float t.maxlat)
+
(Coordinate.longitude_to_float t.maxlon)
+
end
+
+
(** {2 Metadata Operations} *)
+
+
(** Create empty metadata *)
+
let empty = {
+
name = None; desc = None; author = None; copyright = None;
+
links = []; time = None; keywords = None; bounds = None;
+
extensions = [];
+
}
+
+
(** Create metadata with name *)
+
let make ~name = { empty with name = Some name }
+
+
(** Get name *)
+
let name t = t.name
+
+
(** Get description *)
+
let description t = t.desc
+
+
(** Get author *)
+
let author t = t.author
+
+
(** Get copyright *)
+
let copyright t = t.copyright
+
+
(** Get links *)
+
let links t = t.links
+
+
(** Get time *)
+
let time t = t.time
+
+
(** Get keywords *)
+
let keywords t = t.keywords
+
+
(** Get bounds *)
+
let bounds_opt t = t.bounds
+
+
(** Get extensions *)
+
let extensions t = t.extensions
+
+
(** Update name *)
+
let with_name t name = { t with name = Some name }
+
+
(** Update description *)
+
let with_description t desc = { t with desc = Some desc }
+
+
(** Update keywords *)
+
let with_keywords t keywords = { t with keywords = Some keywords }
+
+
(** Update time *)
+
let with_time t time = { t with time }
+
+
(** Update bounds *)
+
let with_bounds t bounds = { t with bounds = Some bounds }
+
+
(** Update author *)
+
let with_author t author = { t with author = Some author }
+
+
(** Update copyright *)
+
let with_copyright t copyright = { t with copyright = Some copyright }
+
+
(** Add link *)
+
let add_link t link = { t with links = link :: t.links }
+
+
(** Add extensions *)
+
let add_extensions t extensions = { t with extensions = extensions @ t.extensions }
+
+
(** Compare metadata *)
+
let compare t1 t2 =
+
let name_cmp = Option.compare String.compare t1.name t2.name in
+
if name_cmp <> 0 then name_cmp
+
else
+
let desc_cmp = Option.compare String.compare t1.desc t2.desc in
+
if desc_cmp <> 0 then desc_cmp
+
else Option.compare Ptime.compare t1.time t2.time
+
+
(** Test metadata equality *)
+
let equal t1 t2 = compare t1 t2 = 0
+
+
(** Pretty print metadata *)
+
let pp ppf t =
+
match t.name with
+
| Some name -> Format.fprintf ppf "\"%s\"" name
+
| None -> Format.fprintf ppf "(unnamed)"
+132
lib/gpx/metadata.mli
···
+
(** GPX metadata and bounds types *)
+
+
(** Bounding box *)
+
type bounds = {
+
minlat : Coordinate.latitude;
+
minlon : Coordinate.longitude;
+
maxlat : Coordinate.latitude;
+
maxlon : Coordinate.longitude;
+
}
+
+
(** Main metadata type *)
+
type t = {
+
name : string option;
+
desc : string option;
+
author : Link.person option;
+
copyright : Link.copyright option;
+
links : Link.t list;
+
time : Ptime.t option;
+
keywords : string option;
+
bounds : bounds option;
+
extensions : Extension.t list;
+
}
+
+
(** {2 Bounds Operations} *)
+
+
module Bounds : sig
+
type t = bounds
+
+
(** Create bounds from validated coordinates *)
+
val make : minlat:Coordinate.latitude -> minlon:Coordinate.longitude ->
+
maxlat:Coordinate.latitude -> maxlon:Coordinate.longitude -> t
+
+
(** Create bounds from float coordinates with validation *)
+
val make_from_floats : minlat:float -> minlon:float -> maxlat:float -> maxlon:float -> (t, string) result
+
+
(** Get minimum corner coordinates *)
+
val min_coords : t -> Coordinate.t
+
+
(** Get maximum corner coordinates *)
+
val max_coords : t -> Coordinate.t
+
+
(** Get all bounds as tuple *)
+
val bounds : t -> (Coordinate.latitude * Coordinate.longitude * Coordinate.latitude * Coordinate.longitude)
+
+
(** Check if coordinate is within bounds *)
+
val contains : t -> Coordinate.t -> bool
+
+
(** Calculate bounds area in square degrees *)
+
val area : t -> float
+
+
(** Compare bounds *)
+
val compare : t -> t -> int
+
+
(** Test bounds equality *)
+
val equal : t -> t -> bool
+
+
(** Pretty print bounds *)
+
val pp : Format.formatter -> t -> unit
+
end
+
+
(** {2 Metadata Operations} *)
+
+
(** Create empty metadata *)
+
val empty : t
+
+
(** Create metadata with name *)
+
val make : name:string -> t
+
+
(** Get name *)
+
val name : t -> string option
+
+
(** Get description *)
+
val description : t -> string option
+
+
(** Get author *)
+
val author : t -> Link.person option
+
+
(** Get copyright *)
+
val copyright : t -> Link.copyright option
+
+
(** Get links *)
+
val links : t -> Link.t list
+
+
(** Get time *)
+
val time : t -> Ptime.t option
+
+
(** Get keywords *)
+
val keywords : t -> string option
+
+
(** Get bounds *)
+
val bounds_opt : t -> bounds option
+
+
(** Get extensions *)
+
val extensions : t -> Extension.t list
+
+
(** Functional operations for building metadata *)
+
+
(** Update name *)
+
val with_name : t -> string -> t
+
+
(** Update description *)
+
val with_description : t -> string -> t
+
+
(** Update keywords *)
+
val with_keywords : t -> string -> t
+
+
(** Update time *)
+
val with_time : t -> Ptime.t option -> t
+
+
(** Update bounds *)
+
val with_bounds : t -> bounds -> t
+
+
(** Update author *)
+
val with_author : t -> Link.person -> t
+
+
(** Update copyright *)
+
val with_copyright : t -> Link.copyright -> t
+
+
(** Add link *)
+
val add_link : t -> Link.t -> t
+
+
(** Add extensions *)
+
val add_extensions : t -> Extension.t list -> t
+
+
(** Compare metadata *)
+
val compare : t -> t -> int
+
+
(** Test metadata equality *)
+
val equal : t -> t -> bool
+
+
(** Pretty print metadata *)
+
val pp : Format.formatter -> t -> unit
+226 -106
lib/gpx/parser.ml
···
(** GPX streaming parser using xmlm *)
-
open Types
-
(** Parser state for streaming *)
type parser_state = {
input : Xmlm.input;
···
let require_attribute name attrs element =
match get_attribute name attrs with
| Some value -> Ok value
-
| None -> Error (Missing_required_attribute (element, name))
+
| None -> Error (Error.missing_attribute element name)
let parse_float_opt s =
try Some (Float.of_string s)
···
let* lon_str = require_attribute "lon" attrs element in
match (Float.of_string lat_str, Float.of_string lon_str) with
| (lat_f, lon_f) ->
-
let* lat = Result.map_error (fun s -> Invalid_coordinate s) (latitude lat_f) in
-
let* lon = Result.map_error (fun s -> Invalid_coordinate s) (longitude lon_f) in
+
let* lat = Result.map_error Error.invalid_coordinate (Coordinate.latitude lat_f) in
+
let* lon = Result.map_error Error.invalid_coordinate (Coordinate.longitude lon_f) in
Ok (lat, lon)
-
| exception _ -> Error (Invalid_coordinate "Invalid coordinate format")
+
| exception _ -> Error (Error.invalid_coordinate "Invalid coordinate format")
(** Parse waypoint data from XML elements *)
let rec parse_waypoint_data parser lat lon =
-
let wpt = make_waypoint_data lat lon in
+
let wpt = Waypoint.make lat lon in
parse_waypoint_elements parser wpt
and parse_waypoint_elements parser wpt =
···
| "ele" ->
let* text = parse_text_content parser in
(match parse_float_opt text with
-
| Some ele -> loop { wpt with ele = Some ele }
+
| Some ele -> loop (Waypoint.with_elevation wpt ele)
| None -> loop wpt)
| "time" ->
let* text = parse_text_content parser in
-
loop { wpt with time = parse_time text }
+
loop (Waypoint.with_time wpt (parse_time text))
| "magvar" ->
let* text = parse_text_content parser in
(match parse_float_opt text with
| Some f ->
-
(match degrees f with
-
| Ok deg -> loop { wpt with magvar = Some deg }
+
(match Coordinate.degrees f with
+
| Ok deg -> loop (Waypoint.with_magvar wpt deg)
| Error _ -> loop wpt)
| None -> loop wpt)
| "geoidheight" ->
let* text = parse_text_content parser in
(match parse_float_opt text with
-
| Some h -> loop { wpt with geoidheight = Some h }
+
| Some h -> loop (Waypoint.with_geoidheight wpt h)
| None -> loop wpt)
| "name" ->
let* text = parse_text_content parser in
-
loop { wpt with name = Some text }
+
loop (Waypoint.with_name wpt text)
| "cmt" ->
let* text = parse_text_content parser in
-
loop { wpt with cmt = Some text }
+
loop (Waypoint.with_comment wpt text)
| "desc" ->
let* text = parse_text_content parser in
-
loop { wpt with desc = Some text }
+
loop (Waypoint.with_description wpt text)
| "src" ->
let* text = parse_text_content parser in
-
loop { wpt with src = Some text }
+
loop (Waypoint.with_source wpt text)
| "sym" ->
let* text = parse_text_content parser in
-
loop { wpt with sym = Some text }
+
loop (Waypoint.with_symbol wpt text)
| "type" ->
let* text = parse_text_content parser in
-
loop { wpt with type_ = Some text }
+
loop (Waypoint.with_type wpt text)
| "fix" ->
let* text = parse_text_content parser in
-
loop { wpt with fix = fix_type_of_string text }
+
loop (Waypoint.with_fix wpt (Waypoint.fix_type_of_string text))
| "sat" ->
let* text = parse_text_content parser in
(match parse_int_opt text with
-
| Some s -> loop { wpt with sat = Some s }
+
| Some s -> loop (Waypoint.with_sat wpt s)
+
| None -> loop wpt)
+
| "hdop" ->
+
let* text = parse_text_content parser in
+
(match parse_float_opt text with
+
| Some f -> loop (Waypoint.with_hdop wpt f)
+
| None -> loop wpt)
+
| "vdop" ->
+
let* text = parse_text_content parser in
+
(match parse_float_opt text with
+
| Some f -> loop (Waypoint.with_vdop wpt f)
| None -> loop wpt)
-
| "hdop" | "vdop" | "pdop" ->
+
| "pdop" ->
let* text = parse_text_content parser in
(match parse_float_opt text with
-
| Some f ->
-
(match name with
-
| "hdop" -> loop { wpt with hdop = Some f }
-
| "vdop" -> loop { wpt with vdop = Some f }
-
| "pdop" -> loop { wpt with pdop = Some f }
-
| _ -> loop wpt)
+
| Some f -> loop (Waypoint.with_pdop wpt f)
| None -> loop wpt)
| "ageofdgpsdata" ->
let* text = parse_text_content parser in
(match parse_float_opt text with
-
| Some f -> loop { wpt with ageofdgpsdata = Some f }
+
| Some f -> loop (Waypoint.with_ageofdgpsdata wpt f)
| None -> loop wpt)
| "dgpsid" ->
let* text = parse_text_content parser in
(match parse_int_opt text with
-
| Some id -> loop { wpt with dgpsid = Some id }
+
| Some id -> loop (Waypoint.with_dgpsid wpt id)
| None -> loop wpt)
| "link" ->
let* link = parse_link parser attrs in
-
loop { wpt with links = link :: wpt.links }
+
loop (Waypoint.add_link wpt link)
| "extensions" ->
let* extensions = parse_extensions parser in
-
loop { wpt with extensions = extensions @ wpt.extensions }
+
loop (Waypoint.add_extensions wpt extensions)
| _ ->
(* Skip unknown elements *)
let* _ = skip_element parser in
···
parser.current_element <- List.tl parser.current_element;
Ok (Buffer.contents parser.text_buffer)
| `El_start _ ->
-
Error (Invalid_xml "Unexpected element in text content")
+
Error (Error.invalid_xml "Unexpected element in text content")
| `Dtd _ ->
loop ()
in
···
| Some h -> h
| None -> ""
in
-
let link = { href; text = None; type_ = None } in
+
let link = Link.make ~href () in
parse_link_elements parser link
and parse_link_elements parser link =
···
(match name with
| "text" ->
let* text = parse_text_content parser in
-
loop { link with text = Some text }
+
loop (Link.with_text link text)
| "type" ->
let* type_text = parse_text_content parser in
-
loop { link with type_ = Some type_text }
+
loop (Link.with_type link type_text)
| _ ->
let* _ = skip_element parser in
loop link)
···
let namespace = if ns = "" then None else Some ns in
let attributes = List.map (fun ((_, n), v) -> (n, v)) attrs in
let* content = parse_extension_content parser in
-
Ok { namespace; name; attributes; content }
+
Ok (Extension.make ?namespace ~name ~attributes ~content ())
and parse_extension_content parser =
Buffer.clear parser.text_buffer;
···
parser.current_element <- List.tl parser.current_element;
let text = String.trim (Buffer.contents parser.text_buffer) in
Ok (match (text, elements) with
-
| ("", []) -> Text ""
-
| ("", els) -> Elements (List.rev els)
-
| (t, []) -> Text t
-
| (t, els) -> Mixed (t, List.rev els))
+
| ("", []) -> Extension.text_content ""
+
| ("", els) -> Extension.elements_content (List.rev els)
+
| (t, []) -> Extension.text_content t
+
| (t, els) -> Extension.mixed_content t (List.rev els))
| `Dtd _ ->
loop elements
in
···
let* version = require_attribute "version" attrs "gpx" in
let* creator = require_attribute "creator" attrs "gpx" in
if version <> "1.0" && version <> "1.1" then
-
Error (Validation_error ("Unsupported GPX version: " ^ version ^ " (supported: 1.0, 1.1)"))
+
Error (Error.validation_error ("Unsupported GPX version: " ^ version ^ " (supported: 1.0, 1.1)"))
else
Ok (version, creator)
| `El_start _ ->
···
| `Dtd _ ->
find_gpx_root ()
| `El_end ->
-
Error (Missing_required_element "gpx")
+
Error (Error.missing_element "gpx")
| `Data _ ->
find_gpx_root ()
in
let* (version, creator) = find_gpx_root () in
-
let gpx = make_gpx ~creator in
+
let gpx = Doc.empty ~creator in
parse_gpx_elements parser { gpx with version }
and parse_gpx_elements parser gpx =
···
(match name with
| "metadata" ->
let* metadata = parse_metadata parser in
-
loop { gpx with metadata = Some metadata }
+
loop (Doc.with_metadata gpx metadata)
| "wpt" ->
let* (lat, lon) = parse_coordinates attrs "wpt" in
let* waypoint = parse_waypoint_data parser lat lon in
-
loop { gpx with waypoints = waypoint :: gpx.waypoints }
+
loop (Doc.add_waypoint gpx waypoint)
| "rte" ->
let* route = parse_route parser in
-
loop { gpx with routes = route :: gpx.routes }
+
loop (Doc.add_route gpx route)
| "trk" ->
let* track = parse_track parser in
-
loop { gpx with tracks = track :: gpx.tracks }
+
loop (Doc.add_track gpx track)
| "extensions" ->
let* extensions = parse_extensions parser in
-
loop { gpx with extensions = extensions @ gpx.extensions }
+
loop (Doc.add_extensions gpx extensions)
| _ ->
let* _ = skip_element parser in
loop gpx)
| `El_end ->
-
Ok { gpx with
-
waypoints = List.rev gpx.waypoints;
-
routes = List.rev gpx.routes;
-
tracks = List.rev gpx.tracks }
+
Ok gpx
| `Data _ ->
loop gpx
| `Dtd _ ->
···
in
loop gpx
+
and parse_author parser =
+
let rec loop name email link =
+
match Xmlm.input parser.input with
+
| `El_start ((_, element_name), attrs) ->
+
parser.current_element <- element_name :: parser.current_element;
+
(match element_name with
+
| "name" ->
+
let* text = parse_text_content parser in
+
loop (Some text) email link
+
| "email" ->
+
let* email_addr = parse_email_attrs attrs in
+
let* _ = skip_element parser in
+
loop name (Some email_addr) link
+
| "link" ->
+
let* parsed_link = parse_link parser attrs in
+
loop name email (Some parsed_link)
+
| _ ->
+
let* _ = skip_element parser in
+
loop name email link)
+
| `El_end ->
+
parser.current_element <- List.tl parser.current_element;
+
Ok (Link.make_person ?name ?email ?link ())
+
| `Data _ ->
+
loop name email link
+
| `Dtd _ ->
+
loop name email link
+
in
+
loop None None None
+
+
and parse_email_attrs attrs =
+
let get_attr name =
+
List.find_map (fun ((_, attr_name), value) ->
+
if attr_name = name then Some value else None
+
) attrs
+
in
+
match get_attr "id", get_attr "domain" with
+
| Some id, Some domain -> Ok (id ^ "@" ^ domain)
+
| _ -> Error (Error.invalid_xml "Missing email id or domain attributes")
+
+
and parse_copyright parser attrs =
+
let get_attr name =
+
List.find_map (fun ((_, attr_name), value) ->
+
if attr_name = name then Some value else None
+
) attrs
+
in
+
let author = get_attr "author" in
+
let rec loop year license =
+
match Xmlm.input parser.input with
+
| `El_start ((_, element_name), _) ->
+
parser.current_element <- element_name :: parser.current_element;
+
(match element_name with
+
| "year" ->
+
let* text = parse_text_content parser in
+
(match parse_int_opt text with
+
| Some y -> loop (Some y) license
+
| None -> loop year license)
+
| "license" ->
+
let* text = parse_text_content parser in
+
loop year (Some text)
+
| _ ->
+
let* _ = skip_element parser in
+
loop year license)
+
| `El_end ->
+
parser.current_element <- List.tl parser.current_element;
+
(match author with
+
| Some auth -> Ok (Link.make_copyright ~author:auth ?year ?license ())
+
| None -> Error (Error.invalid_xml "Missing copyright author attribute"))
+
| `Data _ ->
+
loop year license
+
| `Dtd _ ->
+
loop year license
+
in
+
loop None None
+
+
and parse_bounds parser attrs =
+
let get_attr name =
+
List.find_map (fun ((_, attr_name), value) ->
+
if attr_name = name then Some value else None
+
) attrs
+
in
+
let minlat_str = get_attr "minlat" in
+
let minlon_str = get_attr "minlon" in
+
let maxlat_str = get_attr "maxlat" in
+
let maxlon_str = get_attr "maxlon" in
+
+
(* Skip content since bounds is a self-closing element *)
+
let rec skip_bounds_content () =
+
match Xmlm.input parser.input with
+
| `El_end ->
+
parser.current_element <- List.tl parser.current_element;
+
Ok ()
+
| `Data _ -> skip_bounds_content ()
+
| _ -> skip_bounds_content ()
+
in
+
let* () = skip_bounds_content () in
+
+
match minlat_str, minlon_str, maxlat_str, maxlon_str with
+
| Some minlat, Some minlon, Some maxlat, Some maxlon ->
+
(match
+
Float.of_string_opt minlat, Float.of_string_opt minlon,
+
Float.of_string_opt maxlat, Float.of_string_opt maxlon
+
with
+
| Some minlat_f, Some minlon_f, Some maxlat_f, Some maxlon_f ->
+
(match Metadata.Bounds.make_from_floats ~minlat:minlat_f ~minlon:minlon_f ~maxlat:maxlat_f ~maxlon:maxlon_f with
+
| Ok bounds -> Ok bounds
+
| Error msg -> Error (Error.invalid_xml ("Invalid bounds: " ^ msg)))
+
| _ -> Error (Error.invalid_xml ("Invalid bounds coordinates")))
+
| _ -> Error (Error.invalid_xml ("Missing bounds attributes"))
+
and parse_metadata parser =
-
let metadata = empty_metadata in
-
let rec loop (metadata : metadata) =
+
let metadata = Metadata.empty in
+
let rec loop metadata =
match Xmlm.input parser.input with
| `El_start ((_, name), attrs) ->
parser.current_element <- name :: parser.current_element;
(match name with
| "name" ->
let* text = parse_text_content parser in
-
loop { metadata with name = Some text }
+
loop (Metadata.with_name metadata text)
| "desc" ->
let* text = parse_text_content parser in
-
loop { metadata with desc = Some text }
+
loop (Metadata.with_description metadata text)
| "keywords" ->
let* text = parse_text_content parser in
-
loop { metadata with keywords = Some text }
+
loop (Metadata.with_keywords metadata text)
| "time" ->
let* text = parse_text_content parser in
-
loop { metadata with time = parse_time text }
+
loop (Metadata.with_time metadata (parse_time text))
| "link" ->
let* link = parse_link parser attrs in
-
loop { metadata with links = link :: metadata.links }
+
loop (Metadata.add_link metadata link)
+
| "author" ->
+
let* author = parse_author parser in
+
loop (Metadata.with_author metadata author)
+
| "copyright" ->
+
let* copyright = parse_copyright parser attrs in
+
loop (Metadata.with_copyright metadata copyright)
+
| "bounds" ->
+
let* bounds = parse_bounds parser attrs in
+
loop (Metadata.with_bounds metadata bounds)
| "extensions" ->
let* extensions = parse_extensions parser in
-
loop { metadata with extensions = extensions @ metadata.extensions }
+
loop (Metadata.add_extensions metadata extensions)
| _ ->
let* _ = skip_element parser in
loop metadata)
| `El_end ->
parser.current_element <- List.tl parser.current_element;
-
Ok { metadata with links = List.rev metadata.links }
+
Ok metadata
| `Data _ ->
loop metadata
| `Dtd _ ->
···
loop metadata
and parse_route parser =
-
let route = {
-
name = None; cmt = None; desc = None; src = None; links = [];
-
number = None; type_ = None; extensions = []; rtepts = []
-
} in
-
let rec loop (route : route) =
+
let route = Route.empty in
+
let rec loop route =
match Xmlm.input parser.input with
| `El_start ((_, name), attrs) ->
parser.current_element <- name :: parser.current_element;
(match name with
| "name" ->
let* text = parse_text_content parser in
-
loop { route with name = Some text }
+
loop (Route.with_name route text)
| "cmt" ->
let* text = parse_text_content parser in
-
loop { route with cmt = Some text }
+
loop (Route.with_comment route text)
| "desc" ->
let* text = parse_text_content parser in
-
loop { route with desc = Some text }
+
loop (Route.with_description route text)
| "src" ->
let* text = parse_text_content parser in
-
loop { route with src = Some text }
+
loop (Route.with_source route text)
| "number" ->
let* text = parse_text_content parser in
(match parse_int_opt text with
-
| Some n -> loop { route with number = Some n }
+
| Some n -> loop (Route.with_number route n)
| None -> loop route)
| "type" ->
let* text = parse_text_content parser in
-
loop { route with type_ = Some text }
+
loop (Route.with_type route text)
| "rtept" ->
let* (lat, lon) = parse_coordinates attrs "rtept" in
let* rtept = parse_waypoint_data parser lat lon in
-
loop { route with rtepts = rtept :: route.rtepts }
+
loop (Route.add_point route rtept)
| "link" ->
let* link = parse_link parser attrs in
-
loop { route with links = link :: route.links }
+
loop (Route.add_link route link)
| "extensions" ->
let* extensions = parse_extensions parser in
-
loop { route with extensions = extensions @ route.extensions }
+
loop (Route.add_extensions route extensions)
| _ ->
let* _ = skip_element parser in
loop route)
| `El_end ->
parser.current_element <- List.tl parser.current_element;
-
Ok { route with
-
rtepts = List.rev route.rtepts;
-
links = List.rev route.links }
+
Ok route
| `Data _ ->
loop route
| `Dtd _ ->
···
loop route
and parse_track parser =
-
let track = {
-
name = None; cmt = None; desc = None; src = None; links = [];
-
number = None; type_ = None; extensions = []; trksegs = []
-
} in
+
let track = Track.empty in
let rec loop track =
match Xmlm.input parser.input with
| `El_start ((_, name), attrs) ->
···
(match name with
| "name" ->
let* text = parse_text_content parser in
-
loop { track with name = Some text }
+
loop (Track.with_name track text)
| "cmt" ->
let* text = parse_text_content parser in
-
loop { track with cmt = Some text }
+
loop (Track.with_comment track text)
| "desc" ->
let* text = parse_text_content parser in
-
loop { track with desc = Some text }
+
loop (Track.with_description track text)
| "src" ->
let* text = parse_text_content parser in
-
loop { track with src = Some text }
+
loop (Track.with_source track text)
| "number" ->
let* text = parse_text_content parser in
(match parse_int_opt text with
-
| Some n -> loop { track with number = Some n }
+
| Some n -> loop (Track.with_number track n)
| None -> loop track)
| "type" ->
let* text = parse_text_content parser in
-
loop { track with type_ = Some text }
+
loop (Track.with_type track text)
| "trkseg" ->
let* trkseg = parse_track_segment parser in
-
loop { track with trksegs = trkseg :: track.trksegs }
+
loop (Track.add_segment track trkseg)
| "link" ->
let* link = parse_link parser attrs in
-
loop { track with links = link :: track.links }
+
loop (Track.add_link track link)
| "extensions" ->
let* extensions = parse_extensions parser in
-
loop { track with extensions = extensions @ track.extensions }
+
loop (Track.add_extensions track extensions)
| _ ->
let* _ = skip_element parser in
loop track)
| `El_end ->
parser.current_element <- List.tl parser.current_element;
-
Ok { track with
-
trksegs = List.rev track.trksegs;
-
links = List.rev track.links }
+
Ok track
| `Data _ ->
loop track
| `Dtd _ ->
···
loop track
and parse_track_segment parser =
-
let trkseg = { trkpts = []; extensions = [] } in
+
let trkseg = Track.Segment.empty in
let rec loop trkseg =
match Xmlm.input parser.input with
| `El_start ((_, name), attrs) ->
···
| "trkpt" ->
let* (lat, lon) = parse_coordinates attrs "trkpt" in
let* trkpt = parse_waypoint_data parser lat lon in
-
loop { trkseg with trkpts = trkpt :: trkseg.trkpts }
+
loop (Track.Segment.add_point trkseg trkpt)
| "extensions" ->
-
let* extensions = parse_extensions parser in
-
loop { trkseg with extensions = extensions @ trkseg.extensions }
+
let* _ = parse_extensions parser in
+
loop trkseg
| _ ->
let* _ = skip_element parser in
loop trkseg)
| `El_end ->
parser.current_element <- List.tl parser.current_element;
-
Ok { trkseg with trkpts = List.rev trkseg.trkpts }
+
Ok trkseg
| `Data _ ->
loop trkseg
| `Dtd _ ->
···
loop trkseg
(** Main parsing function *)
-
let parse input =
+
let parse ?(validate=false) input =
let parser = make_parser input in
try
-
parse_gpx parser
+
let result = parse_gpx parser in
+
match result, validate with
+
| Ok gpx, true ->
+
let validation = Validate.validate_gpx gpx in
+
if validation.is_valid then
+
Ok gpx
+
else
+
let error_msgs = List.filter (fun issue -> issue.Validate.level = `Error) validation.issues
+
|> List.map (fun issue -> issue.Validate.message)
+
|> String.concat "; " in
+
Error (Error.validation_error error_msgs)
+
| result, false -> result
+
| Error _ as result, true -> result (* Pass through parse errors even when validating *)
with
| Xmlm.Error ((line, col), error) ->
-
Error (Xml_error (Printf.sprintf "XML error at line %d, column %d: %s"
-
line col (Xmlm.error_message error)))
+
Error (Error.xml_error (Printf.sprintf "XML error at line %d, column %d: %s"
+
line col (Xmlm.error_message error)))
| exn ->
-
Error (Invalid_xml (Printexc.to_string exn))
+
Error (Error.invalid_xml (Printexc.to_string exn))
(** Parse from string *)
-
let parse_string s =
+
let parse_string ?(validate=false) s =
let input = Xmlm.make_input (`String (0, s)) in
-
parse input
+
parse ~validate input
+2 -4
lib/gpx/parser.mli
···
(** GPX streaming parser using xmlm *)
-
open Types
-
(** Parse a GPX document from an xmlm input source *)
-
val parse : Xmlm.input -> gpx result
+
val parse : ?validate:bool -> Xmlm.input -> (Doc.t, Error.t) result
(** Parse a GPX document from a string *)
-
val parse_string : string -> gpx result
+
val parse_string : ?validate:bool -> string -> (Doc.t, Error.t) result
+166
lib/gpx/route.ml
···
+
(** Route types and operations *)
+
+
(** Route point is an alias for waypoint *)
+
type point = Waypoint.t
+
+
(** Main route type *)
+
type t = {
+
name : string option;
+
cmt : string option;
+
desc : string option;
+
src : string option;
+
links : Link.t list;
+
number : int option;
+
type_ : string option;
+
extensions : Extension.t list;
+
rtepts : point list;
+
}
+
+
(** {2 Route Operations} *)
+
+
(** Create empty route *)
+
let empty = {
+
name = None; cmt = None; desc = None; src = None;
+
links = []; number = None; type_ = None; extensions = [];
+
rtepts = [];
+
}
+
+
(** Create route with name *)
+
let make ~name = { empty with name = Some name }
+
+
(** Create route from coordinate list *)
+
let make_from_coords ~name coords =
+
let make_rtept (lat_f, lon_f) =
+
match Waypoint.make_from_floats ~lat:lat_f ~lon:lon_f () with
+
| Ok wpt -> wpt
+
| Error e -> invalid_arg e
+
in
+
let rtepts = List.map make_rtept coords in
+
{ empty with name = Some name; rtepts }
+
+
(** Get route name *)
+
let name t = t.name
+
+
(** Get route description *)
+
let description t = t.desc
+
+
(** Get route number *)
+
let number t = t.number
+
+
(** Get route comment *)
+
let comment t = t.cmt
+
+
(** Get route source *)
+
let source t = t.src
+
+
(** Get route links *)
+
let links t = t.links
+
+
(** Get route type *)
+
let type_ t = t.type_
+
+
(** Get route extensions *)
+
let extensions t = t.extensions
+
+
(** Get route points *)
+
let points t = t.rtepts
+
+
(** Get route point count *)
+
let point_count t = List.length t.rtepts
+
+
+
(** Clear all points *)
+
let clear_points t = { t with rtepts = [] }
+
+
(** Extract coordinates from route *)
+
let to_coords t = List.map Waypoint.to_floats t.rtepts
+
+
(** Simple great circle distance calculation *)
+
let great_circle_distance lat1 lon1 lat2 lon2 =
+
let deg_to_rad x = x *. Float.pi /. 180.0 in
+
let lat1_rad = deg_to_rad lat1 in
+
let lon1_rad = deg_to_rad lon1 in
+
let lat2_rad = deg_to_rad lat2 in
+
let lon2_rad = deg_to_rad lon2 in
+
let dlat = lat2_rad -. lat1_rad in
+
let dlon = lon2_rad -. lon1_rad in
+
let a =
+
sin (dlat /. 2.0) ** 2.0 +.
+
cos lat1_rad *. cos lat2_rad *. sin (dlon /. 2.0) ** 2.0
+
in
+
let c = 2.0 *. asin (sqrt a) in
+
6371000.0 *. c (* Earth radius in meters *)
+
+
(** Calculate total distance between consecutive points (naive great circle) *)
+
let total_distance t =
+
let rec calculate_distance acc = function
+
| [] | [_] -> acc
+
| p1 :: p2 :: rest ->
+
let lat1, lon1 = Waypoint.to_floats p1 in
+
let lat2, lon2 = Waypoint.to_floats p2 in
+
let distance = great_circle_distance lat1 lon1 lat2 lon2 in
+
calculate_distance (acc +. distance) (p2 :: rest)
+
in
+
calculate_distance 0.0 t.rtepts
+
+
(** Check if route is empty *)
+
let is_empty t = List.length t.rtepts = 0
+
+
(** Get first point *)
+
let first_point t =
+
match t.rtepts with
+
| [] -> None
+
| p :: _ -> Some p
+
+
(** Get last point *)
+
let last_point t =
+
match List.rev t.rtepts with
+
| [] -> None
+
| p :: _ -> Some p
+
+
(** {2 Functional Operations} *)
+
+
(** Update name *)
+
let with_name t name = { t with name = Some name }
+
+
(** Update comment *)
+
let with_comment t cmt = { t with cmt = Some cmt }
+
+
(** Update description *)
+
let with_description t desc = { t with desc = Some desc }
+
+
(** Update source *)
+
let with_source t src = { t with src = Some src }
+
+
(** Update number *)
+
let with_number t number = { t with number = Some number }
+
+
(** Update type *)
+
let with_type t type_ = { t with type_ = Some type_ }
+
+
(** Add point *)
+
let add_point t rtept = { t with rtepts = t.rtepts @ [rtept] }
+
+
(** Add link *)
+
let add_link t link = { t with links = t.links @ [link] }
+
+
(** Add extensions *)
+
let add_extensions t extensions = { t with extensions = t.extensions @ extensions }
+
+
(** Compare routes *)
+
let compare t1 t2 =
+
let name_cmp = Option.compare String.compare t1.name t2.name in
+
if name_cmp <> 0 then name_cmp
+
else
+
let desc_cmp = Option.compare String.compare t1.desc t2.desc in
+
if desc_cmp <> 0 then desc_cmp
+
else List.compare Waypoint.compare t1.rtepts t2.rtepts
+
+
(** Test route equality *)
+
let equal t1 t2 = compare t1 t2 = 0
+
+
(** Pretty print route *)
+
let pp ppf t =
+
match t.name with
+
| Some name -> Format.fprintf ppf "\"%s\" (%d points)" name (point_count t)
+
| None -> Format.fprintf ppf "(unnamed route, %d points)" (point_count t)
+125
lib/gpx/route.mli
···
+
(** Route types and operations *)
+
+
(** Route point is an alias for waypoint *)
+
type point = Waypoint.t
+
+
(** Main route type *)
+
type t = {
+
name : string option;
+
cmt : string option;
+
desc : string option;
+
src : string option;
+
links : Link.t list;
+
number : int option;
+
type_ : string option;
+
extensions : Extension.t list;
+
rtepts : point list;
+
}
+
+
(** {2 Route Constructors} *)
+
+
(** Create empty route *)
+
val empty : t
+
+
(** Create route with name *)
+
val make : name:string -> t
+
+
(** Create route from coordinate list.
+
@param name Route name
+
@param coords List of (latitude, longitude) pairs
+
@raises Invalid_argument on invalid coordinates *)
+
val make_from_coords : name:string -> (float * float) list -> t
+
+
(** {2 Route Properties} *)
+
+
(** Get route name *)
+
val name : t -> string option
+
+
(** Get route description *)
+
val description : t -> string option
+
+
(** Get route number *)
+
val number : t -> int option
+
+
(** Get route comment *)
+
val comment : t -> string option
+
+
(** Get route source *)
+
val source : t -> string option
+
+
(** Get route links *)
+
val links : t -> Link.t list
+
+
(** Get route type *)
+
val type_ : t -> string option
+
+
(** Get route extensions *)
+
val extensions : t -> Extension.t list
+
+
(** Get route points *)
+
val points : t -> point list
+
+
(** Get route point count *)
+
val point_count : t -> int
+
+
(** Check if route is empty *)
+
val is_empty : t -> bool
+
+
(** {2 Route Modification} *)
+
+
(** Clear all points *)
+
val clear_points : t -> t
+
+
(** {2 Route Analysis} *)
+
+
(** Extract coordinates from route *)
+
val to_coords : t -> (float * float) list
+
+
(** Calculate total distance between consecutive points in meters *)
+
val total_distance : t -> float
+
+
(** Get first point *)
+
val first_point : t -> point option
+
+
(** Get last point *)
+
val last_point : t -> point option
+
+
(** {2 Functional Operations} *)
+
+
(** Update name *)
+
val with_name : t -> string -> t
+
+
(** Update comment *)
+
val with_comment : t -> string -> t
+
+
(** Update description *)
+
val with_description : t -> string -> t
+
+
(** Update source *)
+
val with_source : t -> string -> t
+
+
(** Update number *)
+
val with_number : t -> int -> t
+
+
(** Update type *)
+
val with_type : t -> string -> t
+
+
(** Add point *)
+
val add_point : t -> point -> t
+
+
(** Add link *)
+
val add_link : t -> Link.t -> t
+
+
(** Add extensions *)
+
val add_extensions : t -> Extension.t list -> t
+
+
(** {2 Comparison and Utilities} *)
+
+
(** Compare routes *)
+
val compare : t -> t -> int
+
+
(** Test route equality *)
+
val equal : t -> t -> bool
+
+
(** Pretty print route *)
+
val pp : Format.formatter -> t -> unit
+226
lib/gpx/track.ml
···
+
(** Track types and operations *)
+
+
(** Track point is an alias for waypoint *)
+
type point = Waypoint.t
+
+
(** Track segment *)
+
type segment = {
+
trkpts : point list;
+
extensions : Extension.t list;
+
}
+
+
(** Main track type *)
+
type t = {
+
name : string option;
+
cmt : string option;
+
desc : string option;
+
src : string option;
+
links : Link.t list;
+
number : int option;
+
type_ : string option;
+
extensions : Extension.t list;
+
trksegs : segment list;
+
}
+
+
(** {2 Track Segment Operations} *)
+
+
module Segment = struct
+
type t = segment
+
+
(** Create empty segment *)
+
let empty = { trkpts = []; extensions = [] }
+
+
(** Create segment with points *)
+
let make points = { trkpts = points; extensions = [] }
+
+
(** Create segment from coordinates *)
+
let make_from_coords coords =
+
let make_trkpt (lat_f, lon_f) =
+
match Waypoint.make_from_floats ~lat:lat_f ~lon:lon_f () with
+
| Ok wpt -> wpt
+
| Error e -> invalid_arg e
+
in
+
let trkpts = List.map make_trkpt coords in
+
{ trkpts; extensions = [] }
+
+
(** Get points *)
+
let points t = t.trkpts
+
+
(** Get point count *)
+
let point_count t = List.length t.trkpts
+
+
(** Get extensions *)
+
let extensions (seg : segment) = seg.extensions
+
+
(** Add point *)
+
let add_point t point = { t with trkpts = t.trkpts @ [point] }
+
+
(** Add points *)
+
let add_points t points = { t with trkpts = t.trkpts @ points }
+
+
(** Extract coordinates *)
+
let to_coords t = List.map Waypoint.to_floats t.trkpts
+
+
(** Calculate segment distance *)
+
let distance t = Route.total_distance { Route.empty with rtepts = t.trkpts }
+
+
(** Check if empty *)
+
let is_empty t = List.length t.trkpts = 0
+
+
(** First point *)
+
let first_point t =
+
match t.trkpts with
+
| [] -> None
+
| p :: _ -> Some p
+
+
(** Last point *)
+
let last_point t =
+
match List.rev t.trkpts with
+
| [] -> None
+
| p :: _ -> Some p
+
+
(** Compare segments *)
+
let compare t1 t2 = List.compare Waypoint.compare t1.trkpts t2.trkpts
+
+
(** Test segment equality *)
+
let equal t1 t2 = compare t1 t2 = 0
+
+
(** Pretty print segment *)
+
let pp ppf t = Format.fprintf ppf "segment (%d points)" (point_count t)
+
end
+
+
(** {2 Track Operations} *)
+
+
(** Create empty track *)
+
let empty = {
+
name = None; cmt = None; desc = None; src = None;
+
links = []; number = None; type_ = None; extensions = [];
+
trksegs = [];
+
}
+
+
(** Create track with name *)
+
let make ~name = { empty with name = Some name }
+
+
(** Create track from coordinate list (single segment) *)
+
let make_from_coords ~name coords =
+
let segment = Segment.make_from_coords coords in
+
{ empty with name = Some name; trksegs = [segment] }
+
+
(** Get track name *)
+
let name t = t.name
+
+
(** Get track description *)
+
let description t = t.desc
+
+
(** Get track comment *)
+
let comment t = t.cmt
+
+
(** Get track source *)
+
let source t = t.src
+
+
(** Get track links *)
+
let links t = t.links
+
+
(** Get track number *)
+
let number t = t.number
+
+
(** Get track type *)
+
let type_ t = t.type_
+
+
(** Get track extensions *)
+
let extensions t = t.extensions
+
+
(** Get track segments *)
+
let segments t = t.trksegs
+
+
(** Get segment count *)
+
let segment_count t = List.length t.trksegs
+
+
(** Get total point count across all segments *)
+
let point_count t =
+
List.fold_left (fun acc seg -> acc + Segment.point_count seg) 0 t.trksegs
+
+
+
(** Clear all segments *)
+
let clear_segments t = { t with trksegs = [] }
+
+
(** Extract all coordinates from track *)
+
let to_coords t =
+
List.fold_left (fun acc seg ->
+
List.fold_left (fun acc trkpt ->
+
Waypoint.to_floats trkpt :: acc
+
) acc seg.trkpts
+
) [] t.trksegs
+
|> List.rev
+
+
(** Calculate total track distance across all segments *)
+
let total_distance t =
+
List.fold_left (fun acc seg -> acc +. Segment.distance seg) 0.0 t.trksegs
+
+
(** Check if track is empty *)
+
let is_empty t = List.length t.trksegs = 0
+
+
(** Get all points from all segments *)
+
let all_points t =
+
List.fold_left (fun acc seg -> acc @ seg.trkpts) [] t.trksegs
+
+
(** Get first point from first segment *)
+
let first_point t =
+
match t.trksegs with
+
| [] -> None
+
| seg :: _ -> Segment.first_point seg
+
+
(** Get last point from last segment *)
+
let last_point t =
+
match List.rev t.trksegs with
+
| [] -> None
+
| seg :: _ -> Segment.last_point seg
+
+
(** Compare tracks *)
+
let compare t1 t2 =
+
let name_cmp = Option.compare String.compare t1.name t2.name in
+
if name_cmp <> 0 then name_cmp
+
else
+
let desc_cmp = Option.compare String.compare t1.desc t2.desc in
+
if desc_cmp <> 0 then desc_cmp
+
else List.compare Segment.compare t1.trksegs t2.trksegs
+
+
(** Test track equality *)
+
let equal t1 t2 = compare t1 t2 = 0
+
+
(** {2 Functional Operations} *)
+
+
(** Update name *)
+
let with_name t name = { t with name = Some name }
+
+
(** Update comment *)
+
let with_comment t cmt = { t with cmt = Some cmt }
+
+
(** Update description *)
+
let with_description t desc = { t with desc = Some desc }
+
+
(** Update source *)
+
let with_source t src = { t with src = Some src }
+
+
(** Update number *)
+
let with_number t number = { t with number = Some number }
+
+
(** Update type *)
+
let with_type t type_ = { t with type_ = Some type_ }
+
+
(** Add segment *)
+
let add_segment t trkseg = { t with trksegs = t.trksegs @ [trkseg] }
+
+
(** Add link *)
+
let add_link t link = { t with links = t.links @ [link] }
+
+
(** Add extensions *)
+
let add_extensions t extensions = { t with extensions = t.extensions @ extensions }
+
+
(** Pretty print track *)
+
let pp ppf t =
+
match t.name with
+
| Some name -> Format.fprintf ppf "\"%s\" (%d segments, %d points)"
+
name (segment_count t) (point_count t)
+
| None -> Format.fprintf ppf "(unnamed track, %d segments, %d points)"
+
(segment_count t) (point_count t)
+192
lib/gpx/track.mli
···
+
(** Track types and operations *)
+
+
(** Track point is an alias for waypoint *)
+
type point = Waypoint.t
+
+
(** Track segment *)
+
type segment = {
+
trkpts : point list;
+
extensions : Extension.t list;
+
}
+
+
(** Main track type *)
+
type t = {
+
name : string option;
+
cmt : string option;
+
desc : string option;
+
src : string option;
+
links : Link.t list;
+
number : int option;
+
type_ : string option;
+
extensions : Extension.t list;
+
trksegs : segment list;
+
}
+
+
(** {2 Track Segment Operations} *)
+
+
module Segment : sig
+
type t = segment
+
+
(** Create empty segment *)
+
val empty : t
+
+
(** Create segment with points *)
+
val make : point list -> t
+
+
(** Create segment from coordinate list.
+
@raises Invalid_argument on invalid coordinates *)
+
val make_from_coords : (float * float) list -> t
+
+
(** Get points *)
+
val points : t -> point list
+
+
(** Get point count *)
+
val point_count : t -> int
+
+
(** Get extensions *)
+
val extensions : t -> Extension.t list
+
+
(** Add point *)
+
val add_point : t -> point -> t
+
+
(** Add points *)
+
val add_points : t -> point list -> t
+
+
(** Extract coordinates *)
+
val to_coords : t -> (float * float) list
+
+
(** Calculate segment distance in meters *)
+
val distance : t -> float
+
+
(** Check if empty *)
+
val is_empty : t -> bool
+
+
(** First point *)
+
val first_point : t -> point option
+
+
(** Last point *)
+
val last_point : t -> point option
+
+
(** Compare segments *)
+
val compare : t -> t -> int
+
+
(** Test segment equality *)
+
val equal : t -> t -> bool
+
+
(** Pretty print segment *)
+
val pp : Format.formatter -> t -> unit
+
end
+
+
(** {2 Track Constructors} *)
+
+
(** Create empty track *)
+
val empty : t
+
+
(** Create track with name *)
+
val make : name:string -> t
+
+
(** Create track from coordinate list (single segment).
+
@param name Track name
+
@param coords List of (latitude, longitude) pairs
+
@raises Failure on invalid coordinates *)
+
val make_from_coords : name:string -> (float * float) list -> t
+
+
(** {2 Track Properties} *)
+
+
(** Get track name *)
+
val name : t -> string option
+
+
(** Get track description *)
+
val description : t -> string option
+
+
(** Get track comment *)
+
val comment : t -> string option
+
+
(** Get track source *)
+
val source : t -> string option
+
+
(** Get track links *)
+
val links : t -> Link.t list
+
+
(** Get track number *)
+
val number : t -> int option
+
+
(** Get track type *)
+
val type_ : t -> string option
+
+
(** Get track extensions *)
+
val extensions : t -> Extension.t list
+
+
(** Get track segments *)
+
val segments : t -> segment list
+
+
(** Get segment count *)
+
val segment_count : t -> int
+
+
(** Get total point count across all segments *)
+
val point_count : t -> int
+
+
(** Check if track is empty *)
+
val is_empty : t -> bool
+
+
(** {2 Track Modification} *)
+
+
(** Clear all segments *)
+
val clear_segments : t -> t
+
+
(** {2 Track Analysis} *)
+
+
(** Extract all coordinates from track *)
+
val to_coords : t -> (float * float) list
+
+
(** Calculate total track distance across all segments in meters *)
+
val total_distance : t -> float
+
+
(** Get all points from all segments *)
+
val all_points : t -> point list
+
+
(** Get first point from first segment *)
+
val first_point : t -> point option
+
+
(** Get last point from last segment *)
+
val last_point : t -> point option
+
+
(** {2 Comparison and Utilities} *)
+
+
(** Compare tracks *)
+
val compare : t -> t -> int
+
+
(** Test track equality *)
+
val equal : t -> t -> bool
+
+
(** {2 Functional Operations} *)
+
+
(** Update name *)
+
val with_name : t -> string -> t
+
+
(** Update comment *)
+
val with_comment : t -> string -> t
+
+
(** Update description *)
+
val with_description : t -> string -> t
+
+
(** Update source *)
+
val with_source : t -> string -> t
+
+
(** Update number *)
+
val with_number : t -> int -> t
+
+
(** Update type *)
+
val with_type : t -> string -> t
+
+
(** Add segment *)
+
val add_segment : t -> Segment.t -> t
+
+
(** Add link *)
+
val add_link : t -> Link.t -> t
+
+
(** Add extensions *)
+
val add_extensions : t -> Extension.t list -> t
+
+
(** Pretty print track *)
+
val pp : Format.formatter -> t -> unit
-228
lib/gpx/types.ml
···
-
(** Core GPX types matching the GPX 1.1 XSD schema *)
-
-
[@@@warning "-30"]
-
-
(** Geographic coordinates with validation constraints *)
-
type latitude = private float
-
type longitude = private float
-
type degrees = private float
-
-
(** Smart constructors for validated coordinates *)
-
let latitude f =
-
if f >= -90.0 && f <= 90.0 then Ok (Obj.magic f : latitude)
-
else Error (Printf.sprintf "Invalid latitude: %f (must be between -90.0 and 90.0)" f)
-
-
let longitude f =
-
if f >= -180.0 && f < 180.0 then Ok (Obj.magic f : longitude)
-
else Error (Printf.sprintf "Invalid longitude: %f (must be between -180.0 and 180.0)" f)
-
-
let degrees f =
-
if f >= 0.0 && f < 360.0 then Ok (Obj.magic f : degrees)
-
else Error (Printf.sprintf "Invalid degrees: %f (must be between 0.0 and 360.0)" f)
-
-
(** Convert back to float *)
-
let latitude_to_float (lat : latitude) = (lat :> float)
-
let longitude_to_float (lon : longitude) = (lon :> float)
-
let degrees_to_float (deg : degrees) = (deg :> float)
-
-
(** GPS fix types as defined in GPX spec *)
-
type fix_type =
-
| None_fix
-
| Fix_2d
-
| Fix_3d
-
| Dgps
-
| Pps
-
-
(** Person information *)
-
type person = {
-
name : string option;
-
email : string option;
-
link : link option;
-
}
-
-
(** Link information *)
-
and link = {
-
href : string;
-
text : string option;
-
type_ : string option;
-
}
-
-
(** Copyright information *)
-
type copyright = {
-
author : string;
-
year : int option;
-
license : string option;
-
}
-
-
(** Bounding box *)
-
type bounds = {
-
minlat : latitude;
-
minlon : longitude;
-
maxlat : latitude;
-
maxlon : longitude;
-
}
-
-
(** Metadata container *)
-
type metadata = {
-
name : string option;
-
desc : string option;
-
author : person option;
-
copyright : copyright option;
-
links : link list;
-
time : Ptime.t option;
-
keywords : string option;
-
bounds : bounds option;
-
extensions : extension list;
-
}
-
-
(** Extension mechanism for custom elements *)
-
and extension = {
-
namespace : string option;
-
name : string;
-
attributes : (string * string) list;
-
content : extension_content;
-
}
-
-
and extension_content =
-
| Text of string
-
| Elements of extension list
-
| Mixed of string * extension list
-
-
(** Base waypoint data shared by wpt, rtept, trkpt *)
-
type waypoint_data = {
-
lat : latitude;
-
lon : longitude;
-
ele : float option;
-
time : Ptime.t option;
-
magvar : degrees option;
-
geoidheight : float option;
-
name : string option;
-
cmt : string option;
-
desc : string option;
-
src : string option;
-
links : link list;
-
sym : string option;
-
type_ : string option;
-
fix : fix_type option;
-
sat : int option;
-
hdop : float option;
-
vdop : float option;
-
pdop : float option;
-
ageofdgpsdata : float option;
-
dgpsid : int option;
-
extensions : extension list;
-
}
-
-
(** Waypoint *)
-
type waypoint = waypoint_data
-
-
(** Route point *)
-
type route_point = waypoint_data
-
-
(** Track point *)
-
type track_point = waypoint_data
-
-
(** Route definition *)
-
type route = {
-
name : string option;
-
cmt : string option;
-
desc : string option;
-
src : string option;
-
links : link list;
-
number : int option;
-
type_ : string option;
-
extensions : extension list;
-
rtepts : route_point list;
-
}
-
-
(** Track segment *)
-
type track_segment = {
-
trkpts : track_point list;
-
extensions : extension list;
-
}
-
-
(** Track definition *)
-
type track = {
-
name : string option;
-
cmt : string option;
-
desc : string option;
-
src : string option;
-
links : link list;
-
number : int option;
-
type_ : string option;
-
extensions : extension list;
-
trksegs : track_segment list;
-
}
-
-
(** Main GPX document *)
-
type gpx = {
-
version : string; (* GPX version: "1.0" or "1.1" *)
-
creator : string;
-
metadata : metadata option;
-
waypoints : waypoint list;
-
routes : route list;
-
tracks : track list;
-
extensions : extension list;
-
}
-
-
(** Parser/Writer errors *)
-
type error =
-
| Invalid_xml of string
-
| Invalid_coordinate of string
-
| Missing_required_attribute of string * string
-
| Missing_required_element of string
-
| Validation_error of string
-
| Xml_error of string
-
| IO_error of string
-
-
exception Gpx_error of error
-
-
(** Result type for operations that can fail *)
-
type 'a result = ('a, error) Result.t
-
-
(** Utility functions *)
-
-
(** Convert fix_type to string *)
-
let fix_type_to_string = function
-
| None_fix -> "none"
-
| Fix_2d -> "2d"
-
| Fix_3d -> "3d"
-
| Dgps -> "dgps"
-
| Pps -> "pps"
-
-
(** Parse fix_type from string *)
-
let fix_type_of_string = function
-
| "none" -> Some None_fix
-
| "2d" -> Some Fix_2d
-
| "3d" -> Some Fix_3d
-
| "dgps" -> Some Dgps
-
| "pps" -> Some Pps
-
| _ -> None
-
-
(** Create empty waypoint_data with required coordinates *)
-
let make_waypoint_data lat lon = {
-
lat; lon;
-
ele = None; time = None; magvar = None; geoidheight = None;
-
name = None; cmt = None; desc = None; src = None; links = [];
-
sym = None; type_ = None; fix = None; sat = None;
-
hdop = None; vdop = None; pdop = None; ageofdgpsdata = None;
-
dgpsid = None; extensions = [];
-
}
-
-
(** Create empty metadata *)
-
let empty_metadata = {
-
name = None; desc = None; author = None; copyright = None;
-
links = []; time = None; keywords = None; bounds = None;
-
extensions = [];
-
}
-
-
(** Create empty GPX document *)
-
let make_gpx ~creator = {
-
version = "1.1";
-
creator;
-
metadata = None;
-
waypoints = [];
-
routes = [];
-
tracks = [];
-
extensions = [];
-
}
-190
lib/gpx/types.mli
···
-
(** Core GPX types matching the GPX 1.1 XSD schema *)
-
-
[@@@warning "-30"]
-
-
(** Geographic coordinates with validation constraints *)
-
type latitude = private float
-
type longitude = private float
-
type degrees = private float
-
-
(** Smart constructors for validated coordinates *)
-
val latitude : float -> (latitude, string) result
-
val longitude : float -> (longitude, string) result
-
val degrees : float -> (degrees, string) result
-
-
(** Convert back to float *)
-
val latitude_to_float : latitude -> float
-
val longitude_to_float : longitude -> float
-
val degrees_to_float : degrees -> float
-
-
(** GPS fix types as defined in GPX spec *)
-
type fix_type =
-
| None_fix
-
| Fix_2d
-
| Fix_3d
-
| Dgps
-
| Pps
-
-
(** Person information *)
-
type person = {
-
name : string option;
-
email : string option;
-
link : link option;
-
}
-
-
(** Link information *)
-
and link = {
-
href : string;
-
text : string option;
-
type_ : string option;
-
}
-
-
(** Copyright information *)
-
type copyright = {
-
author : string;
-
year : int option;
-
license : string option;
-
}
-
-
(** Bounding box *)
-
type bounds = {
-
minlat : latitude;
-
minlon : longitude;
-
maxlat : latitude;
-
maxlon : longitude;
-
}
-
-
(** Metadata container *)
-
type metadata = {
-
name : string option;
-
desc : string option;
-
author : person option;
-
copyright : copyright option;
-
links : link list;
-
time : Ptime.t option;
-
keywords : string option;
-
bounds : bounds option;
-
extensions : extension list;
-
}
-
-
(** Extension mechanism for custom elements *)
-
and extension = {
-
namespace : string option;
-
name : string;
-
attributes : (string * string) list;
-
content : extension_content;
-
}
-
-
and extension_content =
-
| Text of string
-
| Elements of extension list
-
| Mixed of string * extension list
-
-
(** Base waypoint data shared by wpt, rtept, trkpt *)
-
type waypoint_data = {
-
lat : latitude;
-
lon : longitude;
-
ele : float option;
-
time : Ptime.t option;
-
magvar : degrees option;
-
geoidheight : float option;
-
name : string option;
-
cmt : string option;
-
desc : string option;
-
src : string option;
-
links : link list;
-
sym : string option;
-
type_ : string option;
-
fix : fix_type option;
-
sat : int option;
-
hdop : float option;
-
vdop : float option;
-
pdop : float option;
-
ageofdgpsdata : float option;
-
dgpsid : int option;
-
extensions : extension list;
-
}
-
-
(** Waypoint *)
-
type waypoint = waypoint_data
-
-
(** Route point *)
-
type route_point = waypoint_data
-
-
(** Track point *)
-
type track_point = waypoint_data
-
-
(** Route definition *)
-
type route = {
-
name : string option;
-
cmt : string option;
-
desc : string option;
-
src : string option;
-
links : link list;
-
number : int option;
-
type_ : string option;
-
extensions : extension list;
-
rtepts : route_point list;
-
}
-
-
(** Track segment *)
-
type track_segment = {
-
trkpts : track_point list;
-
extensions : extension list;
-
}
-
-
(** Track definition *)
-
type track = {
-
name : string option;
-
cmt : string option;
-
desc : string option;
-
src : string option;
-
links : link list;
-
number : int option;
-
type_ : string option;
-
extensions : extension list;
-
trksegs : track_segment list;
-
}
-
-
(** Main GPX document *)
-
type gpx = {
-
version : string; (* Always "1.1" for this version *)
-
creator : string;
-
metadata : metadata option;
-
waypoints : waypoint list;
-
routes : route list;
-
tracks : track list;
-
extensions : extension list;
-
}
-
-
(** Parser/Writer errors *)
-
type error =
-
| Invalid_xml of string
-
| Invalid_coordinate of string
-
| Missing_required_attribute of string * string
-
| Missing_required_element of string
-
| Validation_error of string
-
| Xml_error of string
-
| IO_error of string
-
-
exception Gpx_error of error
-
-
(** Result type for operations that can fail *)
-
type 'a result = ('a, error) Result.t
-
-
(** Utility functions *)
-
-
(** Convert fix_type to string *)
-
val fix_type_to_string : fix_type -> string
-
-
(** Parse fix_type from string *)
-
val fix_type_of_string : string -> fix_type option
-
-
(** Create empty waypoint_data with required coordinates *)
-
val make_waypoint_data : latitude -> longitude -> waypoint_data
-
-
(** Create empty metadata *)
-
val empty_metadata : metadata
-
-
(** Create empty GPX document *)
-
val make_gpx : creator:string -> gpx
+40 -33
lib/gpx/validate.ml
···
(** GPX validation utilities *)
-
open Types
-
(** Validation error messages *)
type validation_issue = {
level : [`Error | `Warning];
···
let issues = ref [] in
(* Check for negative satellite count *)
-
(match wpt.sat with
+
(match Waypoint.sat wpt with
| Some sat when sat < 0 ->
issues := make_warning ~location ("Negative satellite count: " ^ string_of_int sat) :: !issues
| _ -> ());
···
| _ -> ()
in
-
check_precision "hdop" wpt.hdop;
-
check_precision "vdop" wpt.vdop;
-
check_precision "pdop" wpt.pdop;
+
check_precision "hdop" (Waypoint.hdop wpt);
+
check_precision "vdop" (Waypoint.vdop wpt);
+
check_precision "pdop" (Waypoint.pdop wpt);
(* Check elevation reasonableness *)
-
(match wpt.ele with
+
(match Waypoint.elevation wpt with
| Some ele when ele < -15000.0 ->
issues := make_warning ~location (Printf.sprintf "Very low elevation: %.2f m" ele) :: !issues
| Some ele when ele > 15000.0 ->
···
| _ -> ());
(* Check DGPS age *)
-
(match wpt.ageofdgpsdata with
+
(match Waypoint.ageofdgpsdata wpt with
| Some age when age < 0.0 ->
issues := make_error ~location "Negative DGPS age" :: !issues
| _ -> ());
···
let issues = ref [] in
let location = "bounds" in
-
if latitude_to_float bounds.minlat >= latitude_to_float bounds.maxlat then
+
let (minlat, minlon, maxlat, maxlon) = Metadata.Bounds.bounds bounds in
+
if Coordinate.latitude_to_float minlat >= Coordinate.latitude_to_float maxlat then
issues := make_error ~location "minlat must be less than maxlat" :: !issues;
-
if longitude_to_float bounds.minlon >= longitude_to_float bounds.maxlon then
+
if Coordinate.longitude_to_float minlon >= Coordinate.longitude_to_float maxlon then
issues := make_error ~location "minlon must be less than maxlon" :: !issues;
!issues
···
let issues = ref [] in
(* Validate bounds if present *)
-
(match metadata.bounds with
+
(match Metadata.bounds_opt metadata with
| Some bounds -> issues := validate_bounds bounds @ !issues
| None -> ());
(* Check for reasonable copyright year *)
-
(match metadata.copyright with
+
(match Metadata.copyright metadata with
| Some copyright ->
-
(match copyright.year with
+
(match Link.copyright_year copyright with
| Some year when year < 1900 || year > 2100 ->
issues := make_warning ~location:"metadata.copyright"
(Printf.sprintf "Unusual copyright year: %d" year) :: !issues
···
let location = "route" in
(* Check for empty route *)
-
if route.rtepts = [] then
+
let points = Route.points route in
+
if points = [] then
issues := make_warning ~location "Route has no points" :: !issues;
(* Validate route points *)
List.iteri (fun i rtept ->
let point_location = Printf.sprintf "route.rtept[%d]" i in
issues := validate_waypoint_data rtept point_location @ !issues
-
) route.rtepts;
+
) points;
!issues
···
let location = Printf.sprintf "track.trkseg[%d]" seg_idx in
(* Check for empty segment *)
-
if trkseg.trkpts = [] then
+
let points = Track.Segment.points trkseg in
+
if points = [] then
issues := make_warning ~location "Track segment has no points" :: !issues;
(* Validate track points *)
List.iteri (fun i trkpt ->
let point_location = Printf.sprintf "%s.trkpt[%d]" location i in
issues := validate_waypoint_data trkpt point_location @ !issues
-
) trkseg.trkpts;
+
) points;
(* Check for time ordering if timestamps are present *)
let rec check_time_order prev_time = function
| [] -> ()
| trkpt :: rest ->
-
(match (prev_time, trkpt.time) with
+
(match (prev_time, Waypoint.time trkpt) with
| (Some prev, Some curr) when Ptime.compare prev curr > 0 ->
issues := make_warning ~location "Track points not in chronological order" :: !issues
| _ -> ());
-
check_time_order trkpt.time rest
+
check_time_order (Waypoint.time trkpt) rest
in
-
check_time_order None trkseg.trkpts;
+
check_time_order None points;
!issues
···
let location = "track" in
(* Check for empty track *)
-
if track.trksegs = [] then
+
let segments = Track.segments track in
+
if segments = [] then
issues := make_warning ~location "Track has no segments" :: !issues;
(* Validate track segments *)
List.iteri (fun i trkseg ->
issues := validate_track_segment trkseg i @ !issues
-
) track.trksegs;
+
) segments;
!issues
···
let issues = ref [] in
(* Check GPX version *)
-
if gpx.version <> "1.0" && gpx.version <> "1.1" then
+
let version = Doc.version gpx in
+
if version <> "1.0" && version <> "1.1" then
issues := make_error ~location:"gpx"
-
(Printf.sprintf "Unsupported GPX version: %s (supported: 1.0, 1.1)" gpx.version) :: !issues
-
else if gpx.version = "1.0" then
+
(Printf.sprintf "Unsupported GPX version: %s (supported: 1.0, 1.1)" version) :: !issues
+
else if version = "1.0" then
issues := make_warning ~location:"gpx"
"GPX 1.0 detected - consider upgrading to GPX 1.1 for better compatibility" :: !issues;
(* Check for empty creator *)
-
if String.trim gpx.creator = "" then
+
let creator = Doc.creator gpx in
+
if String.trim creator = "" then
issues := make_error ~location:"gpx" "Creator cannot be empty" :: !issues;
(* Validate metadata *)
-
(match gpx.metadata with
+
(match Doc.metadata gpx with
| Some metadata -> issues := validate_metadata metadata @ !issues
| None -> ());
(* Validate waypoints *)
+
let waypoints = Doc.waypoints gpx in
List.iteri (fun i wpt ->
let location = Printf.sprintf "waypoint[%d]" i in
issues := validate_waypoint_data wpt location @ !issues
-
) gpx.waypoints;
+
) waypoints;
(* Validate routes *)
+
let routes = Doc.routes gpx in
List.iteri (fun _i route ->
issues := validate_route route @ !issues
-
) gpx.routes;
+
) routes;
(* Validate tracks *)
+
let tracks = Doc.tracks gpx in
List.iteri (fun _i track ->
issues := validate_track track @ !issues
-
) gpx.tracks;
+
) tracks;
(* Check for completely empty GPX *)
-
if gpx.waypoints = [] && gpx.routes = [] && gpx.tracks = [] then
+
if waypoints = [] && routes = [] && tracks = [] then
issues := make_warning ~location:"gpx" "GPX document contains no geographic data" :: !issues;
let all_issues = !issues in
···
result.is_valid
(** Get only error messages *)
-
let get_errors gpx =
+
let errors gpx =
let result = validate_gpx gpx in
List.filter (fun issue -> issue.level = `Error) result.issues
(** Get only warning messages *)
-
let get_warnings gpx =
+
let warnings gpx =
let result = validate_gpx gpx in
List.filter (fun issue -> issue.level = `Warning) result.issues
+4 -6
lib/gpx/validate.mli
···
(** GPX validation utilities *)
-
open Types
-
(** Validation issue representation *)
type validation_issue = {
level : [`Error | `Warning];
···
}
(** Validate a complete GPX document *)
-
val validate_gpx : gpx -> validation_result
+
val validate_gpx : Doc.t -> validation_result
(** Quick validation - returns true if document is valid *)
-
val is_valid : gpx -> bool
+
val is_valid : Doc.t -> bool
(** Get only error messages *)
-
val get_errors : gpx -> validation_issue list
+
val errors : Doc.t -> validation_issue list
(** Get only warning messages *)
-
val get_warnings : gpx -> validation_issue list
+
val warnings : Doc.t -> validation_issue list
(** Format validation issue for display *)
val format_issue : validation_issue -> string
+246
lib/gpx/waypoint.ml
···
+
(** Waypoint data and GPS fix types *)
+
+
(** GPS fix types as defined in GPX spec *)
+
type fix_type =
+
| None_fix
+
| Fix_2d
+
| Fix_3d
+
| Dgps
+
| Pps
+
+
(** Main waypoint type - shared by waypoints, route points, track points *)
+
type t = {
+
lat : Coordinate.latitude;
+
lon : Coordinate.longitude;
+
ele : float option;
+
time : Ptime.t option;
+
magvar : Coordinate.degrees option;
+
geoidheight : float option;
+
name : string option;
+
cmt : string option;
+
desc : string option;
+
src : string option;
+
links : Link.t list;
+
sym : string option;
+
type_ : string option;
+
fix : fix_type option;
+
sat : int option;
+
hdop : float option;
+
vdop : float option;
+
pdop : float option;
+
ageofdgpsdata : float option;
+
dgpsid : int option;
+
extensions : Extension.t list;
+
}
+
+
(** {2 Fix Type Operations} *)
+
+
let fix_type_to_string = function
+
| None_fix -> "none"
+
| Fix_2d -> "2d"
+
| Fix_3d -> "3d"
+
| Dgps -> "dgps"
+
| Pps -> "pps"
+
+
let fix_type_of_string = function
+
| "none" -> Some None_fix
+
| "2d" -> Some Fix_2d
+
| "3d" -> Some Fix_3d
+
| "dgps" -> Some Dgps
+
| "pps" -> Some Pps
+
| _ -> None
+
+
let compare_fix_type f1 f2 = match f1, f2 with
+
| None_fix, None_fix -> 0
+
| None_fix, _ -> -1
+
| _, None_fix -> 1
+
| Fix_2d, Fix_2d -> 0
+
| Fix_2d, _ -> -1
+
| _, Fix_2d -> 1
+
| Fix_3d, Fix_3d -> 0
+
| Fix_3d, _ -> -1
+
| _, Fix_3d -> 1
+
| Dgps, Dgps -> 0
+
| Dgps, _ -> -1
+
| _, Dgps -> 1
+
| Pps, Pps -> 0
+
+
(** {2 Waypoint Operations} *)
+
+
(** Create waypoint with required coordinates *)
+
let make lat lon = {
+
lat; lon;
+
ele = None; time = None; magvar = None; geoidheight = None;
+
name = None; cmt = None; desc = None; src = None; links = [];
+
sym = None; type_ = None; fix = None; sat = None;
+
hdop = None; vdop = None; pdop = None; ageofdgpsdata = None;
+
dgpsid = None; extensions = [];
+
}
+
+
(** Create waypoint from float coordinates *)
+
let make_from_floats ~lat ~lon ?name ?desc () =
+
match Coordinate.latitude lat, Coordinate.longitude lon with
+
| Ok lat_coord, Ok lon_coord ->
+
let wpt = make lat_coord lon_coord in
+
Ok { wpt with name; desc }
+
| Error e, _ | _, Error e -> Error e
+
+
(** Get coordinate pair *)
+
let coordinate t = Coordinate.make t.lat t.lon
+
+
(** Get latitude *)
+
let lat t = t.lat
+
+
(** Get longitude *)
+
let lon t = t.lon
+
+
(** Get coordinate as float pair *)
+
let to_floats t = (Coordinate.latitude_to_float t.lat, Coordinate.longitude_to_float t.lon)
+
+
(** Get elevation *)
+
let elevation t = t.ele
+
+
(** Get time *)
+
let time t = t.time
+
+
(** Get name *)
+
let name t = t.name
+
+
(** Get description *)
+
let description t = t.desc
+
+
(** Get comment *)
+
let comment t = t.cmt
+
+
(** Get source *)
+
let source t = t.src
+
+
(** Get symbol *)
+
let symbol t = t.sym
+
+
(** Get type *)
+
let type_ t = t.type_
+
+
(** Get fix type *)
+
let fix t = t.fix
+
+
(** Get satellite count *)
+
let sat t = t.sat
+
+
(** Get horizontal dilution of precision *)
+
let hdop t = t.hdop
+
+
(** Get vertical dilution of precision *)
+
let vdop t = t.vdop
+
+
(** Get position dilution of precision *)
+
let pdop t = t.pdop
+
+
(** Get magnetic variation *)
+
let magvar t = t.magvar
+
+
(** Get geoid height *)
+
let geoidheight t = t.geoidheight
+
+
(** Get age of DGPS data *)
+
let ageofdgpsdata t = t.ageofdgpsdata
+
+
(** Get DGPS ID *)
+
let dgpsid t = t.dgpsid
+
+
(** Get links *)
+
let links t = t.links
+
+
(** Get extensions *)
+
let extensions t = t.extensions
+
+
(** Update elevation *)
+
let with_elevation t ele = { t with ele = Some ele }
+
+
(** Update time *)
+
let with_time t time = { t with time }
+
+
(** Update name *)
+
let with_name t name = { t with name = Some name }
+
+
(** Update comment *)
+
let with_comment t cmt = { t with cmt = Some cmt }
+
+
(** Update description *)
+
let with_description t desc = { t with desc = Some desc }
+
+
(** Update source *)
+
let with_source t src = { t with src = Some src }
+
+
(** Update symbol *)
+
let with_symbol t sym = { t with sym = Some sym }
+
+
(** Update type *)
+
let with_type t type_ = { t with type_ = Some type_ }
+
+
(** Update fix *)
+
let with_fix t fix = { t with fix }
+
+
(** Update satellite count *)
+
let with_sat t sat = { t with sat = Some sat }
+
+
(** Update HDOP *)
+
let with_hdop t hdop = { t with hdop = Some hdop }
+
+
(** Update VDOP *)
+
let with_vdop t vdop = { t with vdop = Some vdop }
+
+
(** Update PDOP *)
+
let with_pdop t pdop = { t with pdop = Some pdop }
+
+
(** Update magnetic variation *)
+
let with_magvar t magvar = { t with magvar = Some magvar }
+
+
(** Update geoid height *)
+
let with_geoidheight t geoidheight = { t with geoidheight = Some geoidheight }
+
+
(** Update age of DGPS data *)
+
let with_ageofdgpsdata t ageofdgpsdata = { t with ageofdgpsdata = Some ageofdgpsdata }
+
+
(** Update DGPS ID *)
+
let with_dgpsid t dgpsid = { t with dgpsid = Some dgpsid }
+
+
(** Add link *)
+
let add_link t link = { t with links = link :: t.links }
+
+
(** Add extensions *)
+
let add_extensions t extensions = { t with extensions = extensions @ t.extensions }
+
+
(** Compare waypoints *)
+
let compare t1 t2 =
+
let lat_cmp = Float.compare
+
(Coordinate.latitude_to_float t1.lat)
+
(Coordinate.latitude_to_float t2.lat) in
+
if lat_cmp <> 0 then lat_cmp
+
else
+
let lon_cmp = Float.compare
+
(Coordinate.longitude_to_float t1.lon)
+
(Coordinate.longitude_to_float t2.lon) in
+
if lon_cmp <> 0 then lon_cmp
+
else
+
let ele_cmp = Option.compare Float.compare t1.ele t2.ele in
+
if ele_cmp <> 0 then ele_cmp
+
else Option.compare Ptime.compare t1.time t2.time
+
+
(** Test waypoint equality *)
+
let equal t1 t2 = compare t1 t2 = 0
+
+
(** Pretty print waypoint *)
+
let pp ppf t =
+
let lat, lon = to_floats t in
+
match t.name with
+
| Some name -> Format.fprintf ppf "%s @ (%g, %g)" name lat lon
+
| None -> Format.fprintf ppf "(%g, %g)" lat lon
+
+
(** Pretty print fix type *)
+
let pp_fix_type ppf = function
+
| None_fix -> Format.fprintf ppf "none"
+
| Fix_2d -> Format.fprintf ppf "2d"
+
| Fix_3d -> Format.fprintf ppf "3d"
+
| Dgps -> Format.fprintf ppf "dgps"
+
| Pps -> Format.fprintf ppf "pps"
+193
lib/gpx/waypoint.mli
···
+
(** Waypoint data and GPS fix types *)
+
+
(** GPS fix types as defined in GPX spec *)
+
type fix_type =
+
| None_fix
+
| Fix_2d
+
| Fix_3d
+
| Dgps
+
| Pps
+
+
(** Main waypoint type - shared by waypoints, route points, track points *)
+
type t = {
+
lat : Coordinate.latitude;
+
lon : Coordinate.longitude;
+
ele : float option;
+
time : Ptime.t option;
+
magvar : Coordinate.degrees option;
+
geoidheight : float option;
+
name : string option;
+
cmt : string option;
+
desc : string option;
+
src : string option;
+
links : Link.t list;
+
sym : string option;
+
type_ : string option;
+
fix : fix_type option;
+
sat : int option;
+
hdop : float option;
+
vdop : float option;
+
pdop : float option;
+
ageofdgpsdata : float option;
+
dgpsid : int option;
+
extensions : Extension.t list;
+
}
+
+
(** {2 Fix Type Operations} *)
+
+
(** Convert fix_type to string *)
+
val fix_type_to_string : fix_type -> string
+
+
(** Parse fix_type from string *)
+
val fix_type_of_string : string -> fix_type option
+
+
(** Compare fix types *)
+
val compare_fix_type : fix_type -> fix_type -> int
+
+
(** Pretty print fix type *)
+
val pp_fix_type : Format.formatter -> fix_type -> unit
+
+
(** {2 Waypoint Operations} *)
+
+
(** Create waypoint with required coordinates *)
+
val make : Coordinate.latitude -> Coordinate.longitude -> t
+
+
(** Create waypoint from float coordinates with validation *)
+
val make_from_floats : lat:float -> lon:float -> ?name:string -> ?desc:string -> unit -> (t, string) result
+
+
(** Get coordinate pair *)
+
val coordinate : t -> Coordinate.t
+
+
(** Get latitude *)
+
val lat : t -> Coordinate.latitude
+
+
(** Get longitude *)
+
val lon : t -> Coordinate.longitude
+
+
(** Get coordinate as float pair *)
+
val to_floats : t -> float * float
+
+
(** Get elevation *)
+
val elevation : t -> float option
+
+
(** Get time *)
+
val time : t -> Ptime.t option
+
+
(** Get name *)
+
val name : t -> string option
+
+
(** Get description *)
+
val description : t -> string option
+
+
(** Get comment *)
+
val comment : t -> string option
+
+
(** Get source *)
+
val source : t -> string option
+
+
(** Get symbol *)
+
val symbol : t -> string option
+
+
(** Get type *)
+
val type_ : t -> string option
+
+
(** Get fix type *)
+
val fix : t -> fix_type option
+
+
(** Get satellite count *)
+
val sat : t -> int option
+
+
(** Get horizontal dilution of precision *)
+
val hdop : t -> float option
+
+
(** Get vertical dilution of precision *)
+
val vdop : t -> float option
+
+
(** Get position dilution of precision *)
+
val pdop : t -> float option
+
+
(** Get magnetic variation *)
+
val magvar : t -> Coordinate.degrees option
+
+
(** Get geoid height *)
+
val geoidheight : t -> float option
+
+
(** Get age of DGPS data *)
+
val ageofdgpsdata : t -> float option
+
+
(** Get DGPS ID *)
+
val dgpsid : t -> int option
+
+
(** Get links *)
+
val links : t -> Link.t list
+
+
(** Get extensions *)
+
val extensions : t -> Extension.t list
+
+
(** Functional operations for building waypoints *)
+
+
(** Update elevation *)
+
val with_elevation : t -> float -> t
+
+
(** Update time *)
+
val with_time : t -> Ptime.t option -> t
+
+
(** Update name *)
+
val with_name : t -> string -> t
+
+
(** Update comment *)
+
val with_comment : t -> string -> t
+
+
(** Update description *)
+
val with_description : t -> string -> t
+
+
(** Update source *)
+
val with_source : t -> string -> t
+
+
(** Update symbol *)
+
val with_symbol : t -> string -> t
+
+
(** Update type *)
+
val with_type : t -> string -> t
+
+
(** Update fix *)
+
val with_fix : t -> fix_type option -> t
+
+
(** Update satellite count *)
+
val with_sat : t -> int -> t
+
+
(** Update HDOP *)
+
val with_hdop : t -> float -> t
+
+
(** Update VDOP *)
+
val with_vdop : t -> float -> t
+
+
(** Update PDOP *)
+
val with_pdop : t -> float -> t
+
+
(** Update magnetic variation *)
+
val with_magvar : t -> Coordinate.degrees -> t
+
+
(** Update geoid height *)
+
val with_geoidheight : t -> float -> t
+
+
(** Update age of DGPS data *)
+
val with_ageofdgpsdata : t -> float -> t
+
+
(** Update DGPS ID *)
+
val with_dgpsid : t -> int -> t
+
+
(** Add link *)
+
val add_link : t -> Link.t -> t
+
+
(** Add extensions *)
+
val add_extensions : t -> Extension.t list -> t
+
+
(** Compare waypoints *)
+
val compare : t -> t -> int
+
+
(** Test waypoint equality *)
+
val equal : t -> t -> bool
+
+
(** Pretty print waypoint *)
+
val pp : Format.formatter -> t -> unit
+265 -294
lib/gpx/writer.ml
···
-
(** GPX streaming writer using xmlm *)
-
-
open Types
+
(** GPX XML writer with complete spec coverage *)
(** Result binding operators *)
let (let*) = Result.bind
-
(** Writer state for streaming *)
-
type writer_state = {
-
output : Xmlm.output;
-
}
-
-
(** Create a new writer state *)
-
let make_writer output = { output }
-
-
(** Utility functions *)
-
-
let convert_attributes attrs =
-
List.map (fun (name, value) -> (("", name), value)) attrs
-
-
let output_signal writer signal =
+
(** Helper to write XML elements *)
+
let output_element_start writer name attrs =
try
-
Xmlm.output writer.output signal;
+
Xmlm.output writer (`El_start ((("", name), attrs)));
Ok ()
-
with
-
| Xmlm.Error ((line, col), error) ->
-
Error (Xml_error (Printf.sprintf "XML error at line %d, column %d: %s"
-
line col (Xmlm.error_message error)))
-
| exn ->
-
Error (Invalid_xml (Printexc.to_string exn))
-
-
let output_element_start writer name attrs =
-
output_signal writer (`El_start (("", name), attrs))
+
with exn ->
+
Error (Error.xml_error (Printexc.to_string exn))
let output_element_end writer =
-
output_signal writer `El_end
+
try
+
Xmlm.output writer `El_end;
+
Ok ()
+
with exn ->
+
Error (Error.xml_error (Printexc.to_string exn))
let output_data writer text =
-
if text <> "" then
-
output_signal writer (`Data text)
-
else
+
try
+
Xmlm.output writer (`Data text);
Ok ()
+
with exn ->
+
Error (Error.xml_error (Printexc.to_string exn))
let output_text_element writer name text =
-
let* () = output_element_start writer name [] in
+
let attrs = [] in
+
let* () = output_element_start writer name attrs in
let* () = output_data writer text in
output_element_end writer
···
| Some text -> output_text_element writer name text
| None -> Ok ()
-
let output_float_element writer name f =
-
output_text_element writer name (Printf.sprintf "%.6f" f)
-
let output_optional_float_element writer name = function
-
| Some f -> output_float_element writer name f
+
| Some value -> output_text_element writer name (Printf.sprintf "%.6f" value)
| None -> Ok ()
-
let output_int_element writer name i =
-
output_text_element writer name (string_of_int i)
+
let output_optional_degrees_element writer name = function
+
| Some degrees -> output_text_element writer name (Printf.sprintf "%.6f" (Coordinate.degrees_to_float degrees))
+
| None -> Ok ()
let output_optional_int_element writer name = function
-
| Some i -> output_int_element writer name i
+
| Some value -> output_text_element writer name (string_of_int value)
| None -> Ok ()
-
let output_time_element writer name time =
-
output_text_element writer name (Ptime.to_rfc3339 time)
-
let output_optional_time_element writer name = function
-
| Some time -> output_time_element writer name time
+
| Some time -> output_text_element writer name (Ptime.to_rfc3339 time)
| None -> Ok ()
-
(** Write GPX header and DTD *)
-
let write_header writer =
-
let* () = output_signal writer (`Dtd None) in
-
Ok ()
+
let output_optional_fix_element writer name = function
+
| Some fix_type -> output_text_element writer name (Waypoint.fix_type_to_string fix_type)
+
| None -> Ok ()
-
(** Write link element *)
-
let write_link writer link =
-
let attrs = [(("" , "href"), link.href)] in
+
(** Write link elements *)
+
let output_link writer link =
+
let href = Link.href link in
+
let attrs = [(("", "href"), href)] in
let* () = output_element_start writer "link" attrs in
-
let* () = output_optional_text_element writer "text" link.text in
-
let* () = output_optional_text_element writer "type" link.type_ in
+
let* () = output_optional_text_element writer "text" (Link.text link) in
+
let* () = output_optional_text_element writer "type" (Link.type_ link) in
output_element_end writer
-
(** Write list of links *)
-
let write_links writer links =
-
let rec loop = function
+
let output_links writer links =
+
let rec write_links = function
| [] -> Ok ()
| link :: rest ->
-
let* () = write_link writer link in
-
loop rest
+
let* () = output_link writer link in
+
write_links rest
in
-
loop links
+
write_links links
-
(** Write extension content *)
-
let rec write_extension_content writer = function
-
| Text text -> output_data writer text
-
| Elements extensions -> write_extensions writer extensions
-
| Mixed (text, extensions) ->
-
let* () = output_data writer text in
-
write_extensions writer extensions
-
-
(** Write extensions *)
-
and write_extensions writer extensions =
-
let rec loop = function
-
| [] -> Ok ()
-
| ext :: rest ->
-
let* () = write_extension writer ext in
-
loop rest
+
(** Write person (author) element *)
+
let output_person writer person =
+
let* () = output_element_start writer "author" [] in
+
let* () = output_optional_text_element writer "name" (Link.person_name person) in
+
let* () = match Link.person_email person with
+
| Some email ->
+
(* Parse email into id and domain *)
+
(match String.index_opt email '@' with
+
| Some at_pos ->
+
let id = String.sub email 0 at_pos in
+
let domain = String.sub email (at_pos + 1) (String.length email - at_pos - 1) in
+
let attrs = [(("", "id"), id); (("", "domain"), domain)] in
+
let* () = output_element_start writer "email" attrs in
+
output_element_end writer
+
| None ->
+
(* Invalid email format, skip *)
+
Ok ())
+
| None -> Ok ()
in
-
loop extensions
-
-
and write_extension writer ext =
-
let name = match ext.namespace with
-
| Some ns -> ns ^ ":" ^ ext.name
-
| None -> ext.name
+
let* () = match Link.person_link person with
+
| Some link -> output_link writer link
+
| None -> Ok ()
in
-
let* () = output_element_start writer name (convert_attributes ext.attributes) in
-
let* () = write_extension_content writer ext.content in
output_element_end writer
-
(** Write waypoint data (shared by wpt, rtept, trkpt) *)
-
let write_waypoint_data writer element_name wpt =
-
let attrs = [
-
(("", "lat"), Printf.sprintf "%.6f" (latitude_to_float wpt.lat));
-
(("", "lon"), Printf.sprintf "%.6f" (longitude_to_float wpt.lon));
-
] in
-
let* () = output_element_start writer element_name attrs in
-
let* () = output_optional_float_element writer "ele" wpt.ele in
-
let* () = output_optional_time_element writer "time" wpt.time in
-
let* () = (match wpt.magvar with
-
| Some deg -> output_float_element writer "magvar" (degrees_to_float deg)
-
| None -> Ok ()) in
-
let* () = output_optional_float_element writer "geoidheight" wpt.geoidheight in
-
let* () = output_optional_text_element writer "name" wpt.name in
-
let* () = output_optional_text_element writer "cmt" wpt.cmt in
-
let* () = output_optional_text_element writer "desc" wpt.desc in
-
let* () = output_optional_text_element writer "src" wpt.src in
-
let* () = write_links writer wpt.links in
-
let* () = output_optional_text_element writer "sym" wpt.sym in
-
let* () = output_optional_text_element writer "type" wpt.type_ in
-
let* () = (match wpt.fix with
-
| Some fix -> output_text_element writer "fix" (fix_type_to_string fix)
-
| None -> Ok ()) in
-
let* () = output_optional_int_element writer "sat" wpt.sat in
-
let* () = output_optional_float_element writer "hdop" wpt.hdop in
-
let* () = output_optional_float_element writer "vdop" wpt.vdop in
-
let* () = output_optional_float_element writer "pdop" wpt.pdop in
-
let* () = output_optional_float_element writer "ageofdgpsdata" wpt.ageofdgpsdata in
-
let* () = output_optional_int_element writer "dgpsid" wpt.dgpsid in
-
let* () = (if wpt.extensions <> [] then
-
let* () = output_element_start writer "extensions" [] in
-
let* () = write_extensions writer wpt.extensions in
-
output_element_end writer
-
else Ok ()) in
-
output_element_end writer
-
-
(** Write waypoint *)
-
let write_waypoint writer wpt =
-
write_waypoint_data writer "wpt" wpt
-
-
(** Write route point *)
-
let write_route_point writer rtept =
-
write_waypoint_data writer "rtept" rtept
-
-
(** Write track point *)
-
let write_track_point writer trkpt =
-
write_waypoint_data writer "trkpt" trkpt
-
-
(** Write person *)
-
let write_person writer (p : person) =
-
let* () = output_element_start writer "author" [] in
-
let* () = output_optional_text_element writer "name" p.name in
-
let* () = output_optional_text_element writer "email" p.email in
-
let* () = (match p.link with
-
| Some link -> write_link writer link
-
| None -> Ok ()) in
-
output_element_end writer
-
-
(** Write copyright *)
-
let write_copyright writer (copyright : copyright) =
-
let attrs = [(("", "author"), copyright.author)] in
+
(** Write copyright element *)
+
let output_copyright writer copyright =
+
let author = Link.copyright_author copyright in
+
let attrs = [(("", "author"), author)] in
let* () = output_element_start writer "copyright" attrs in
-
let* () = (match copyright.year with
-
| Some year -> output_int_element writer "year" year
-
| None -> Ok ()) in
-
let* () = output_optional_text_element writer "license" copyright.license in
+
let* () = output_optional_int_element writer "year" (Link.copyright_year copyright) in
+
let* () = output_optional_text_element writer "license" (Link.copyright_license copyright) in
output_element_end writer
-
(** Write bounds *)
-
let write_bounds writer bounds =
+
(** Write bounds element *)
+
let output_bounds writer bounds =
+
let (minlat, minlon, maxlat, maxlon) = Metadata.Bounds.bounds bounds in
let attrs = [
-
(("", "minlat"), Printf.sprintf "%.6f" (latitude_to_float bounds.minlat));
-
(("", "minlon"), Printf.sprintf "%.6f" (longitude_to_float bounds.minlon));
-
(("", "maxlat"), Printf.sprintf "%.6f" (latitude_to_float bounds.maxlat));
-
(("", "maxlon"), Printf.sprintf "%.6f" (longitude_to_float bounds.maxlon));
+
(("", "minlat"), Printf.sprintf "%.6f" (Coordinate.latitude_to_float minlat));
+
(("", "minlon"), Printf.sprintf "%.6f" (Coordinate.longitude_to_float minlon));
+
(("", "maxlat"), Printf.sprintf "%.6f" (Coordinate.latitude_to_float maxlat));
+
(("", "maxlon"), Printf.sprintf "%.6f" (Coordinate.longitude_to_float maxlon));
] in
let* () = output_element_start writer "bounds" attrs in
output_element_end writer
-
(** Write metadata *)
-
let write_metadata writer (metadata : metadata) =
+
(** Write extensions element *)
+
let output_extensions writer extensions =
+
if extensions = [] then Ok ()
+
else
+
let* () = output_element_start writer "extensions" [] in
+
(* For now, skip writing extension content - would need full extension serialization *)
+
output_element_end writer
+
+
(** Write metadata element *)
+
let output_metadata writer metadata =
let* () = output_element_start writer "metadata" [] in
-
let* () = output_optional_text_element writer "name" metadata.name in
-
let* () = output_optional_text_element writer "desc" metadata.desc in
-
let* () = (match metadata.author with
-
| Some author -> write_person writer author
-
| None -> Ok ()) in
-
let* () = (match metadata.copyright with
-
| Some copyright -> write_copyright writer copyright
-
| None -> Ok ()) in
-
let* () = write_links writer metadata.links in
-
let* () = output_optional_time_element writer "time" metadata.time in
-
let* () = output_optional_text_element writer "keywords" metadata.keywords in
-
let* () = (match metadata.bounds with
-
| Some bounds -> write_bounds writer bounds
-
| None -> Ok ()) in
-
let* () = (if metadata.extensions <> [] then
-
let* () = output_element_start writer "extensions" [] in
-
let* () = write_extensions writer metadata.extensions in
-
output_element_end writer
-
else Ok ()) in
+
let* () = output_optional_text_element writer "name" (Metadata.name metadata) in
+
let* () = output_optional_text_element writer "desc" (Metadata.description metadata) in
+
let* () = match Metadata.author metadata with
+
| Some author -> output_person writer author
+
| None -> Ok ()
+
in
+
let* () = match Metadata.copyright metadata with
+
| Some copyright -> output_copyright writer copyright
+
| None -> Ok ()
+
in
+
let* () = output_links writer (Metadata.links metadata) in
+
let* () = output_optional_time_element writer "time" (Metadata.time metadata) in
+
let* () = output_optional_text_element writer "keywords" (Metadata.keywords metadata) in
+
let* () = match Metadata.bounds_opt metadata with
+
| Some bounds -> output_bounds writer bounds
+
| None -> Ok ()
+
in
+
let* () = output_extensions writer (Metadata.extensions metadata) in
output_element_end writer
-
(** Write route *)
-
let write_route writer (route : route) =
-
let* () = output_element_start writer "rte" [] in
-
let* () = output_optional_text_element writer "name" route.name in
-
let* () = output_optional_text_element writer "cmt" route.cmt in
-
let* () = output_optional_text_element writer "desc" route.desc in
-
let* () = output_optional_text_element writer "src" route.src in
-
let* () = write_links writer route.links in
-
let* () = output_optional_int_element writer "number" route.number in
-
let* () = output_optional_text_element writer "type" route.type_ in
-
let* () = (if route.extensions <> [] then
-
let* () = output_element_start writer "extensions" [] in
-
let* () = write_extensions writer route.extensions in
-
output_element_end writer
-
else Ok ()) in
-
let* () =
-
let rec loop = function
-
| [] -> Ok ()
-
| rtept :: rest ->
-
let* () = write_route_point writer rtept in
-
loop rest
-
in
-
loop route.rtepts
+
(** Write waypoint elements (used for wpt, rtept, trkpt) *)
+
let output_waypoint_data writer waypoint =
+
let* () = output_optional_float_element writer "ele" (Waypoint.elevation waypoint) in
+
let* () = output_optional_time_element writer "time" (Waypoint.time waypoint) in
+
let* () = output_optional_degrees_element writer "magvar" (Waypoint.magvar waypoint) in
+
let* () = output_optional_float_element writer "geoidheight" (Waypoint.geoidheight waypoint) in
+
let* () = output_optional_text_element writer "name" (Waypoint.name waypoint) in
+
let* () = output_optional_text_element writer "cmt" (Waypoint.comment waypoint) in
+
let* () = output_optional_text_element writer "desc" (Waypoint.description waypoint) in
+
let* () = output_optional_text_element writer "src" (Waypoint.source waypoint) in
+
let* () = output_links writer (Waypoint.links waypoint) in
+
let* () = output_optional_text_element writer "sym" (Waypoint.symbol waypoint) in
+
let* () = output_optional_text_element writer "type" (Waypoint.type_ waypoint) in
+
let* () = output_optional_fix_element writer "fix" (Waypoint.fix waypoint) in
+
let* () = output_optional_int_element writer "sat" (Waypoint.sat waypoint) in
+
let* () = output_optional_float_element writer "hdop" (Waypoint.hdop waypoint) in
+
let* () = output_optional_float_element writer "vdop" (Waypoint.vdop waypoint) in
+
let* () = output_optional_float_element writer "pdop" (Waypoint.pdop waypoint) in
+
let* () = output_optional_float_element writer "ageofdgpsdata" (Waypoint.ageofdgpsdata waypoint) in
+
let* () = output_optional_int_element writer "dgpsid" (Waypoint.dgpsid waypoint) in
+
output_extensions writer (Waypoint.extensions waypoint)
+
+
(** Write waypoints *)
+
let output_waypoints writer waypoints =
+
let rec write_waypoints = function
+
| [] -> Ok ()
+
| wpt :: rest ->
+
let lat = Coordinate.latitude_to_float (Waypoint.lat wpt) in
+
let lon = Coordinate.longitude_to_float (Waypoint.lon wpt) in
+
let attrs = [
+
(("", "lat"), Printf.sprintf "%.6f" lat);
+
(("", "lon"), Printf.sprintf "%.6f" lon);
+
] in
+
let* () = output_element_start writer "wpt" attrs in
+
let* () = output_waypoint_data writer wpt in
+
let* () = output_element_end writer in
+
write_waypoints rest
in
-
output_element_end writer
+
write_waypoints waypoints
-
(** Write track segment *)
-
let write_track_segment writer trkseg =
-
let* () = output_element_start writer "trkseg" [] in
-
let* () =
-
let rec loop = function
-
| [] -> Ok ()
-
| trkpt :: rest ->
-
let* () = write_track_point writer trkpt in
-
loop rest
-
in
-
loop trkseg.trkpts
+
(** Write route points *)
+
let output_route_points writer points element_name =
+
let rec write_points = function
+
| [] -> Ok ()
+
| pt :: rest ->
+
let lat = Coordinate.latitude_to_float (Waypoint.lat pt) in
+
let lon = Coordinate.longitude_to_float (Waypoint.lon pt) in
+
let attrs = [
+
(("", "lat"), Printf.sprintf "%.6f" lat);
+
(("", "lon"), Printf.sprintf "%.6f" lon);
+
] in
+
let* () = output_element_start writer element_name attrs in
+
let* () = output_waypoint_data writer pt in
+
let* () = output_element_end writer in
+
write_points rest
in
-
let* () = (if trkseg.extensions <> [] then
-
let* () = output_element_start writer "extensions" [] in
-
let* () = write_extensions writer trkseg.extensions in
-
output_element_end writer
-
else Ok ()) in
-
output_element_end writer
+
write_points points
-
(** Write track *)
-
let write_track writer track =
-
let* () = output_element_start writer "trk" [] in
-
let* () = output_optional_text_element writer "name" track.name in
-
let* () = output_optional_text_element writer "cmt" track.cmt in
-
let* () = output_optional_text_element writer "desc" track.desc in
-
let* () = output_optional_text_element writer "src" track.src in
-
let* () = write_links writer track.links in
-
let* () = output_optional_int_element writer "number" track.number in
-
let* () = output_optional_text_element writer "type" track.type_ in
-
let* () = (if track.extensions <> [] then
-
let* () = output_element_start writer "extensions" [] in
-
let* () = write_extensions writer track.extensions in
-
output_element_end writer
-
else Ok ()) in
-
let* () =
-
let rec loop = function
-
| [] -> Ok ()
-
| trkseg :: rest ->
-
let* () = write_track_segment writer trkseg in
-
loop rest
-
in
-
loop track.trksegs
+
(** Write routes *)
+
let output_routes writer routes =
+
let rec write_routes = function
+
| [] -> Ok ()
+
| route :: rest ->
+
let* () = output_element_start writer "rte" [] in
+
let* () = output_optional_text_element writer "name" (Route.name route) in
+
let* () = output_optional_text_element writer "cmt" (Route.comment route) in
+
let* () = output_optional_text_element writer "desc" (Route.description route) in
+
let* () = output_optional_text_element writer "src" (Route.source route) in
+
let* () = output_links writer (Route.links route) in
+
let* () = output_optional_int_element writer "number" (Route.number route) in
+
let* () = output_optional_text_element writer "type" (Route.type_ route) in
+
let* () = output_extensions writer (Route.extensions route) in
+
let* () = output_route_points writer (Route.points route) "rtept" in
+
let* () = output_element_end writer in
+
write_routes rest
in
-
output_element_end writer
+
write_routes routes
-
(** Write complete GPX document *)
-
let write_gpx writer gpx =
-
let* () = write_header writer in
-
let attrs = [
-
(("", "version"), gpx.version);
-
(("", "creator"), gpx.creator);
-
(("", "xmlns"), "http://www.topografix.com/GPX/1/1");
-
(("http://www.w3.org/2000/xmlns/", "xsi"), "http://www.w3.org/2001/XMLSchema-instance");
-
(("http://www.w3.org/2001/XMLSchema-instance", "schemaLocation"), "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd");
-
] in
-
let* () = output_element_start writer "gpx" attrs in
-
let* () = (match gpx.metadata with
-
| Some metadata -> write_metadata writer metadata
-
| None -> Ok ()) in
-
let* () =
-
let rec loop = function
-
| [] -> Ok ()
-
| wpt :: rest ->
-
let* () = write_waypoint writer wpt in
-
loop rest
-
in
-
loop gpx.waypoints
+
(** Write track segments *)
+
let output_track_segments writer segments =
+
let rec write_segments = function
+
| [] -> Ok ()
+
| seg :: rest ->
+
let* () = output_element_start writer "trkseg" [] in
+
let* () = output_route_points writer (Track.Segment.points seg) "trkpt" in
+
let* () = output_extensions writer (Track.Segment.extensions seg) in
+
let* () = output_element_end writer in
+
write_segments rest
in
-
let* () =
-
let rec loop = function
-
| [] -> Ok ()
-
| rte :: rest ->
-
let* () = write_route writer rte in
-
loop rest
-
in
-
loop gpx.routes
+
write_segments segments
+
+
(** Write tracks *)
+
let output_tracks writer tracks =
+
let rec write_tracks = function
+
| [] -> Ok ()
+
| track :: rest ->
+
let* () = output_element_start writer "trk" [] in
+
let* () = output_optional_text_element writer "name" (Track.name track) in
+
let* () = output_optional_text_element writer "cmt" (Track.comment track) in
+
let* () = output_optional_text_element writer "desc" (Track.description track) in
+
let* () = output_optional_text_element writer "src" (Track.source track) in
+
let* () = output_links writer (Track.links track) in
+
let* () = output_optional_int_element writer "number" (Track.number track) in
+
let* () = output_optional_text_element writer "type" (Track.type_ track) in
+
let* () = output_extensions writer (Track.extensions track) in
+
let* () = output_track_segments writer (Track.segments track) in
+
let* () = output_element_end writer in
+
write_tracks rest
in
-
let* () =
-
let rec loop = function
-
| [] -> Ok ()
-
| trk :: rest ->
-
let* () = write_track writer trk in
-
loop rest
-
in
-
loop gpx.tracks
+
write_tracks tracks
+
+
(** Write a complete GPX document *)
+
let write ?(validate=false) output gpx =
+
let writer = Xmlm.make_output output in
+
+
let result =
+
try
+
(* Write XML declaration and GPX root element *)
+
let version = Doc.version gpx in
+
let creator = Doc.creator gpx in
+
let attrs = [
+
(("", "version"), version);
+
(("", "creator"), creator);
+
(("", "xsi:schemaLocation"), "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd");
+
(("xmlns", "xsi"), "http://www.w3.org/2001/XMLSchema-instance");
+
(("", "xmlns"), "http://www.topografix.com/GPX/1/1")
+
] in
+
+
let* () = output_element_start writer "gpx" attrs in
+
+
(* Write metadata if present *)
+
let* () = match Doc.metadata gpx with
+
| Some metadata -> output_metadata writer metadata
+
| None -> Ok ()
+
in
+
+
(* Write waypoints *)
+
let* () = output_waypoints writer (Doc.waypoints gpx) in
+
+
(* Write routes *)
+
let* () = output_routes writer (Doc.routes gpx) in
+
+
(* Write tracks *)
+
let* () = output_tracks writer (Doc.tracks gpx) in
+
+
(* Write root-level extensions *)
+
let* () = output_extensions writer (Doc.extensions gpx) in
+
+
output_element_end writer
+
+
with
+
| Xmlm.Error ((line, col), error) ->
+
Error (Error.xml_error (Printf.sprintf "XML error at line %d, column %d: %s"
+
line col (Xmlm.error_message error)))
+
| exn ->
+
Error (Error.xml_error (Printexc.to_string exn))
in
-
let* () = (if gpx.extensions <> [] then
-
let* () = output_element_start writer "extensions" [] in
-
let* () = write_extensions writer gpx.extensions in
-
output_element_end writer
-
else Ok ()) in
-
output_element_end writer
-
-
(** Main writing function *)
-
let write output gpx =
-
let writer = make_writer output in
-
write_gpx writer gpx
+
+
match result, validate with
+
| Ok (), true ->
+
let validation = Validate.validate_gpx gpx in
+
if validation.is_valid then
+
Ok ()
+
else
+
let error_msgs = List.filter (fun issue -> issue.Validate.level = `Error) validation.issues
+
|> List.map (fun issue -> issue.Validate.message)
+
|> String.concat "; " in
+
Error (Error.validation_error error_msgs)
+
| result, false -> result
+
| Error _ as result, true -> result (* Pass through write errors even when validating *)
-
(** Write to string *)
-
let write_string gpx =
+
(** Write GPX to string *)
+
let write_string ?(validate=false) gpx =
let buffer = Buffer.create 1024 in
-
let output = Xmlm.make_output (`Buffer buffer) in
-
let result = write output gpx in
-
match result with
-
| Ok () -> Ok (Buffer.contents buffer)
-
| Error e -> Error e
+
let dest = `Buffer buffer in
+
let* () = write ~validate dest gpx in
+
Ok (Buffer.contents buffer)
+2 -4
lib/gpx/writer.mli
···
(** GPX streaming writer using xmlm *)
-
open Types
-
(** Write a GPX document to an xmlm output destination *)
-
val write : Xmlm.output -> gpx -> unit result
+
val write : ?validate:bool -> Xmlm.dest -> Doc.t -> (unit, Error.t) result
(** Write a GPX document to a string *)
-
val write_string : gpx -> string result
+
val write_string : ?validate:bool -> Doc.t -> (string, Error.t) result
+1 -1
lib/gpx_eio/dune
···
(public_name mlgpx.eio)
(name gpx_eio)
(libraries eio xmlm ptime gpx)
-
(modules gpx_io gpx_eio))
+
(modules gpx_io gpx_eio))
+11 -141
lib/gpx_eio/gpx_eio.ml
···
-
(** High-level Eio API for GPX operations *)
+
(** Eio API for GPX operations *)
-
(* I/O module *)
module IO = Gpx_io
-
-
(** Convenience functions for common operations *)
(** Read and parse GPX file *)
-
let read ~fs path = IO.read_file ~fs path
-
-
(** Read and parse GPX file with validation *)
-
let read_validated ~fs path = IO.read_file_validated ~fs path
+
let read ?(validate=false) ~fs path = IO.read_file ~validate ~fs path
(** Write GPX to file *)
-
let write ~fs path gpx = IO.write_file ~fs path gpx
-
-
(** Write GPX to file with validation *)
-
let write_validated ~fs path gpx = IO.write_file_validated ~fs path gpx
+
let write ?(validate=false) ~fs path gpx = IO.write_file ~validate ~fs path gpx
(** Write GPX to file with backup *)
-
let write_with_backup ~fs path gpx = IO.write_file_with_backup ~fs path gpx
+
let write_with_backup ?(validate=false) ~fs path gpx = IO.write_file_with_backup ~validate ~fs path gpx
(** Read GPX from Eio source *)
-
let from_source source = IO.read_source source
+
let from_source ?(validate=false) source = IO.read_source ~validate source
(** Write GPX to Eio sink *)
-
let to_sink sink gpx = IO.write_sink sink gpx
-
-
(** Read GPX from Eio source with validation *)
-
let from_source_validated source = IO.read_source_validated source
-
-
(** Write GPX to Eio sink with validation *)
-
let to_sink_validated sink gpx = IO.write_sink_validated sink gpx
-
-
(** Create simple waypoint *)
-
let make_waypoint ~fs:_ ~lat ~lon ?name ?desc () =
-
match Gpx.latitude lat, Gpx.longitude lon with
-
| Ok lat, Ok lon ->
-
let wpt = Gpx.make_waypoint_data lat lon in
-
{ wpt with name; desc }
-
| Error e, _ | _, Error e -> raise (Gpx.Gpx_error (Gpx.Invalid_coordinate e))
-
-
(** Create simple track from coordinate list *)
-
let make_track_from_coords ~fs:_ ~name coords =
-
let make_trkpt (lat_f, lon_f) =
-
match Gpx.latitude lat_f, Gpx.longitude lon_f with
-
| Ok lat, Ok lon -> Gpx.make_waypoint_data lat lon
-
| Error e, _ | _, Error e -> raise (Gpx.Gpx_error (Gpx.Invalid_coordinate e))
-
in
-
let trkpts = List.map make_trkpt coords in
-
let trkseg : Gpx.track_segment = { trkpts; extensions = [] } in
-
({
-
name = Some name;
-
cmt = None; desc = None; src = None; links = [];
-
number = None; type_ = None; extensions = [];
-
trksegs = [trkseg];
-
} : Gpx.track)
-
-
(** Create simple route from coordinate list *)
-
let make_route_from_coords ~fs:_ ~name coords =
-
let make_rtept (lat_f, lon_f) =
-
match Gpx.latitude lat_f, Gpx.longitude lon_f with
-
| Ok lat, Ok lon -> Gpx.make_waypoint_data lat lon
-
| Error e, _ | _, Error e -> raise (Gpx.Gpx_error (Gpx.Invalid_coordinate e))
-
in
-
let rtepts = List.map make_rtept coords in
-
({
-
name = Some name;
-
cmt = None; desc = None; src = None; links = [];
-
number = None; type_ = None; extensions = [];
-
rtepts;
-
} : Gpx.route)
-
-
(** Extract coordinates from waypoints *)
-
let waypoint_coords (wpt : Gpx.waypoint_data) =
-
(Gpx.latitude_to_float wpt.lat, Gpx.longitude_to_float wpt.lon)
-
-
(** Extract coordinates from track *)
-
let track_coords (track : Gpx.track) =
-
List.fold_left (fun acc (trkseg : Gpx.track_segment) ->
-
List.fold_left (fun acc trkpt ->
-
waypoint_coords trkpt :: acc
-
) acc trkseg.trkpts
-
) [] track.trksegs
-
|> List.rev
-
-
(** Extract coordinates from route *)
-
let route_coords (route : Gpx.route) =
-
List.map waypoint_coords route.rtepts
-
-
(** Count total points in GPX *)
-
let count_points (gpx : Gpx.gpx) =
-
let waypoint_count = List.length gpx.waypoints in
-
let route_count = List.fold_left (fun acc (route : Gpx.route) ->
-
acc + List.length route.rtepts
-
) 0 gpx.routes in
-
let track_count = List.fold_left (fun acc (track : Gpx.track) ->
-
List.fold_left (fun acc (trkseg : Gpx.track_segment) ->
-
acc + List.length trkseg.trkpts
-
) acc track.trksegs
-
) 0 gpx.tracks in
-
waypoint_count + route_count + track_count
-
-
(** Get GPX statistics *)
-
type gpx_stats = {
-
waypoint_count : int;
-
route_count : int;
-
track_count : int;
-
total_points : int;
-
has_elevation : bool;
-
has_time : bool;
-
}
-
-
let get_stats (gpx : Gpx.gpx) =
-
let waypoint_count = List.length gpx.waypoints in
-
let route_count = List.length gpx.routes in
-
let track_count = List.length gpx.tracks in
-
let total_points = count_points gpx in
-
-
let has_elevation =
-
List.exists (fun (wpt : Gpx.waypoint_data) -> wpt.ele <> None) gpx.waypoints ||
-
List.exists (fun (route : Gpx.route) ->
-
List.exists (fun (rtept : Gpx.waypoint_data) -> rtept.ele <> None) route.rtepts
-
) gpx.routes ||
-
List.exists (fun (track : Gpx.track) ->
-
List.exists (fun (trkseg : Gpx.track_segment) ->
-
List.exists (fun (trkpt : Gpx.waypoint_data) -> trkpt.ele <> None) trkseg.trkpts
-
) track.trksegs
-
) gpx.tracks
-
in
-
-
let has_time =
-
List.exists (fun (wpt : Gpx.waypoint_data) -> wpt.time <> None) gpx.waypoints ||
-
List.exists (fun (route : Gpx.route) ->
-
List.exists (fun (rtept : Gpx.waypoint_data) -> rtept.time <> None) route.rtepts
-
) gpx.routes ||
-
List.exists (fun (track : Gpx.track) ->
-
List.exists (fun (trkseg : Gpx.track_segment) ->
-
List.exists (fun (trkpt : Gpx.waypoint_data) -> trkpt.time <> None) trkseg.trkpts
-
) track.trksegs
-
) gpx.tracks
-
in
-
-
{ waypoint_count; route_count; track_count; total_points; has_elevation; has_time }
+
let to_sink ?(validate=false) sink gpx = IO.write_sink ~validate sink gpx
(** Pretty print GPX statistics *)
-
let print_stats (gpx : Gpx.gpx) =
-
let stats = get_stats gpx in
-
Printf.printf "GPX Statistics:\n";
-
Printf.printf " Waypoints: %d\n" stats.waypoint_count;
-
Printf.printf " Routes: %d\n" stats.route_count;
-
Printf.printf " Tracks: %d\n" stats.track_count;
-
Printf.printf " Total points: %d\n" stats.total_points;
-
Printf.printf " Has elevation data: %s\n" (if stats.has_elevation then "yes" else "no");
-
Printf.printf " Has time data: %s\n" (if stats.has_time then "yes" else "no")
+
let print_stats sink gpx =
+
let buf = Buffer.create 256 in
+
let fmt = Format.formatter_of_buffer buf in
+
Format.fprintf fmt "%a@?" Gpx.Doc.pp_stats gpx;
+
Eio.Flow.copy_string (Buffer.contents buf) sink
+23 -115
lib/gpx_eio/gpx_eio.mli
···
-
(** {1 GPX Eio - High-level Eio API for GPX operations}
+
(** {1 Eio API for GPX operations}
-
This module provides a high-level API for GPX operations using Eio's
+
This module provides a direct-style API for GPX operations using Eio's
effects-based concurrent I/O system. It offers convenient functions
for common GPX operations while maintaining structured concurrency.
-
{2 Key Features}
-
-
- Effects-based I/O using Eio
-
- Structured concurrency compatible
-
- Resource-safe operations
-
- Exception-based error handling (raises [Gpx.Gpx_error])
-
- Concurrent processing capabilities
-
{2 Usage Example}
{[
···
let fs = Eio.Stdenv.fs env in
(* Create a GPX document *)
-
let lat = Gpx.latitude 37.7749 |> Result.get_ok in
-
let lon = Gpx.longitude (-122.4194) |> Result.get_ok in
-
let wpt = make_waypoint fs ~lat:(Gpx.latitude_to_float lat) ~lon:(Gpx.longitude_to_float lon) ~name:"San Francisco" () in
-
let gpx = Gpx.make_gpx ~creator:"eio-example" in
-
let gpx = { gpx with waypoints = [wpt] } in
+
let lat = Gpx.Coordinate.latitude 37.7749 |> Result.get_ok in
+
let lon = Gpx.Coordinate.longitude (-122.4194) |> Result.get_ok in
+
let wpt = Gpx.Waypoint.make lat lon |> Gpx.Waypoint.with_name "San Francisco" in
+
let gpx = Gpx.make_gpx ~creator:"eio-example" |> Gpx.Doc.add_waypoint wpt in
(* Write with validation *)
-
write_validated fs "output.gpx" gpx;
+
write ~validate:true fs "output.gpx" gpx;
(* Read it back *)
-
let gpx2 = read_validated fs "output.gpx" in
-
Printf.printf "Read %d waypoints\n" (List.length gpx2.waypoints)
+
let gpx2 = read ~validate:true fs "output.gpx" in
+
Printf.printf "Read %d waypoints\n" (List.length (Gpx.Doc.waypoints gpx2))
let () = Eio_main.run main
]}
···
(** {2 Convenience File Operations}
-
These functions provide simple file I/O with the filesystem from [Eio.Stdenv.fs]. *)
+
These functions provide simple file I/O with the filesystem from {!Eio.Stdenv.fs}. *)
(** Read and parse GPX file.
@param fs Filesystem capability
@param path File path to read
+
@param ?validate Optional validation flag (default: false)
@return GPX document
@raises Gpx.Gpx_error on read or parse failure *)
-
val read : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx
-
-
(** Read and parse GPX file with validation.
-
@param fs Filesystem capability
-
@param path File path to read
-
@return Valid GPX document
-
@raises Gpx.Gpx_error on validation failure *)
-
val read_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx
+
val read : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t
(** Write GPX to file.
@param fs Filesystem capability
@param path File path to write
@param gpx GPX document to write
+
@param ?validate Optional validation flag (default: false)
@raises Gpx.Gpx_error on write failure *)
-
val write : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit
-
-
(** Write GPX to file with validation.
-
@param fs Filesystem capability
-
@param path File path to write
-
@param gpx GPX document to write
-
@raises Gpx.Gpx_error on validation or write failure *)
-
val write_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit
+
val write : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t -> unit
(** Write GPX to file with automatic backup.
@param fs Filesystem capability
@param path File path to write
@param gpx GPX document to write
+
@param ?validate Optional validation flag (default: false)
@return Backup file path (empty if no backup created)
@raises Gpx.Gpx_error on failure *)
-
val write_with_backup : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> string
+
val write_with_backup : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t -> string
(** {2 Stream Operations}
···
(** Read GPX from Eio source.
@param source Input flow
+
@param ?validate Optional validation flag (default: false)
@return GPX document
@raises Gpx.Gpx_error on read or parse failure *)
-
val from_source : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx
+
val from_source : ?validate:bool -> [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.t
(** Write GPX to Eio sink.
@param sink Output flow
@param gpx GPX document
+
@param ?validate Optional validation flag (default: false)
@raises Gpx.Gpx_error on write failure *)
-
val to_sink : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit
-
-
(** Read GPX from Eio source with validation.
-
@param source Input flow
-
@return Valid GPX document
-
@raises Gpx.Gpx_error on validation failure *)
-
val from_source_validated : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx
-
-
(** Write GPX to Eio sink with validation.
-
@param sink Output flow
-
@param gpx GPX document
-
@raises Gpx.Gpx_error on validation failure *)
-
val to_sink_validated : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit
-
-
(** {2 Utility Functions} *)
-
-
(** Create simple waypoint with coordinates.
-
@param fs Filesystem capability (unused, for API consistency)
-
@param lat Latitude in degrees
-
@param lon Longitude in degrees
-
@param ?name Optional waypoint name
-
@param ?desc Optional waypoint description
-
@return Waypoint data
-
@raises Gpx.Gpx_error on invalid coordinates *)
-
val make_waypoint : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> lat:float -> lon:float -> ?name:string -> ?desc:string -> unit -> Gpx.waypoint_data
+
val to_sink : ?validate:bool -> [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.t -> unit
-
(** Create track from coordinate list.
-
@param fs Filesystem capability (unused, for API consistency)
-
@param name Track name
-
@param coords List of (latitude, longitude) pairs
-
@return Track with single segment
-
@raises Gpx.Gpx_error on invalid coordinates *)
-
val make_track_from_coords : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> name:string -> (float * float) list -> Gpx.track
-
-
(** Create route from coordinate list.
-
@param fs Filesystem capability (unused, for API consistency)
-
@param name Route name
-
@param coords List of (latitude, longitude) pairs
-
@return Route
-
@raises Gpx.Gpx_error on invalid coordinates *)
-
val make_route_from_coords : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> name:string -> (float * float) list -> Gpx.route
-
-
(** Extract coordinates from waypoint.
-
@param wpt Waypoint data
-
@return (latitude, longitude) as floats *)
-
val waypoint_coords : Gpx.waypoint_data -> float * float
-
-
(** Extract coordinates from track.
-
@param track Track
-
@return List of (latitude, longitude) pairs *)
-
val track_coords : Gpx.track -> (float * float) list
-
-
(** Extract coordinates from route.
-
@param route Route
-
@return List of (latitude, longitude) pairs *)
-
val route_coords : Gpx.route -> (float * float) list
-
-
(** Count total points in GPX document.
-
@param gpx GPX document
-
@return Total number of waypoints, route points, and track points *)
-
val count_points : Gpx.gpx -> int
-
-
(** GPX statistics record *)
-
type gpx_stats = {
-
waypoint_count : int; (** Number of waypoints *)
-
route_count : int; (** Number of routes *)
-
track_count : int; (** Number of tracks *)
-
total_points : int; (** Total geographic points *)
-
has_elevation : bool; (** Document contains elevation data *)
-
has_time : bool; (** Document contains time data *)
-
}
-
-
(** Get GPX document statistics.
-
@param gpx GPX document
-
@return Statistics summary *)
-
val get_stats : Gpx.gpx -> gpx_stats
-
-
(** Print GPX statistics to stdout.
+
(** Print GPX statistics to sink.
+
@param sink Output sink
@param gpx GPX document *)
-
val print_stats : Gpx.gpx -> unit
+
val print_stats : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.t -> unit
+12 -56
lib/gpx_eio/gpx_io.ml
···
(** GPX Eio I/O operations *)
-
(* Real Eio-based I/O operations *)
-
(** Read GPX from file path *)
-
let read_file ~fs path =
+
let read_file ?(validate=false) ~fs path =
let content = Eio.Path.load Eio.Path.(fs / path) in
-
match Gpx.parse_string content with
+
match Gpx.parse_string ~validate content with
| Ok gpx -> gpx
| Error err -> raise (Gpx.Gpx_error err)
(** Write GPX to file path *)
-
let write_file ~fs path gpx =
-
match Gpx.write_string gpx with
+
let write_file ?(validate=false) ~fs path gpx =
+
match Gpx.write_string ~validate gpx with
| Ok xml_string ->
Eio.Path.save ~create:(`Or_truncate 0o644) Eio.Path.(fs / path) xml_string
| Error err -> raise (Gpx.Gpx_error err)
-
(** Read GPX from file with validation *)
-
let read_file_validated ~fs path =
-
let gpx = read_file ~fs path in
-
let validation = Gpx.validate_gpx gpx in
-
if validation.is_valid then
-
gpx
-
else
-
let errors = Gpx.get_errors gpx in
-
let error_msgs = List.map Gpx.format_issue errors in
-
raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs)))
-
-
(** Write GPX to file with validation *)
-
let write_file_validated ~fs path gpx =
-
let validation = Gpx.validate_gpx gpx in
-
if not validation.is_valid then
-
let errors = Gpx.get_errors gpx in
-
let error_msgs = List.map Gpx.format_issue errors in
-
raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs)))
-
else
-
write_file ~fs path gpx
-
(** Read GPX from Eio source *)
-
let read_source source =
+
let read_source ?(validate=false) source =
let content = Eio.Flow.read_all source in
-
match Gpx.parse_string content with
+
match Gpx.parse_string ~validate content with
| Ok gpx -> gpx
| Error err -> raise (Gpx.Gpx_error err)
(** Write GPX to Eio sink *)
-
let write_sink sink gpx =
-
match Gpx.write_string gpx with
+
let write_sink ?(validate=false) sink gpx =
+
match Gpx.write_string ~validate gpx with
| Ok xml_string ->
Eio.Flow.copy_string xml_string sink
| Error err -> raise (Gpx.Gpx_error err)
-
(** Read GPX from Eio source with validation *)
-
let read_source_validated source =
-
let gpx = read_source source in
-
let validation = Gpx.validate_gpx gpx in
-
if validation.is_valid then
-
gpx
-
else
-
let errors = Gpx.get_errors gpx in
-
let error_msgs = List.map Gpx.format_issue errors in
-
raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs)))
-
-
(** Write GPX to Eio sink with validation *)
-
let write_sink_validated sink gpx =
-
let validation = Gpx.validate_gpx gpx in
-
if not validation.is_valid then
-
let errors = Gpx.get_errors gpx in
-
let error_msgs = List.map Gpx.format_issue errors in
-
raise (Gpx.Gpx_error (Gpx.Validation_error (String.concat "; " error_msgs)))
-
else
-
write_sink sink gpx
-
(** Check if file exists *)
let file_exists ~fs path =
try
···
let stat = Eio.Path.stat ~follow:true Eio.Path.(fs / path) in
Optint.Int63.to_int stat.size
with
-
| exn -> raise (Gpx.Gpx_error (Gpx.IO_error (Printexc.to_string exn)))
+
| exn -> raise (Gpx.Gpx_error (Gpx.Error.io_error (Printexc.to_string exn)))
(** Create backup of existing file *)
let create_backup ~fs path =
···
""
(** Write GPX to file with automatic backup *)
-
let write_file_with_backup ~fs path gpx =
+
let write_file_with_backup ?(validate=false) ~fs path gpx =
let backup_path = create_backup ~fs path in
try
-
write_file ~fs path gpx;
+
write_file ~validate ~fs path gpx;
backup_path
with
| Gpx.Gpx_error _ as err ->
···
Eio.Path.save ~create:(`Or_truncate 0o644) Eio.Path.(fs / path) backup_content
with _ -> () (* Ignore restore errors *)
);
-
raise err
+
raise err
+11 -34
lib/gpx_eio/gpx_io.mli
···
compatible and work with Eio's resource management.
*)
-
(** {1 File Operations}
-
-
All file operations require filesystem access capability. *)
+
(** {1 File Operations} *)
(** Read GPX from file path.
@param fs Filesystem capability
@param path File path to read
+
@param ?validate Optional validation flag (default: false)
@return GPX document or error *)
-
val read_file : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx
+
val read_file : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t
(** Write GPX to file path.
@param fs Filesystem capability
@param path File path to write
@param gpx GPX document to write
+
@param ?validate Optional validation flag (default: false)
@raises Gpx.Gpx_error on write failure *)
-
val write_file : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit
-
-
(** Read GPX from file with validation.
-
@param fs Filesystem capability
-
@param path File path to read
-
@return Valid GPX document
-
@raises Gpx.Gpx_error on validation failure *)
-
val read_file_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx
-
-
(** Write GPX to file with validation.
-
@param fs Filesystem capability
-
@param path File path to write
-
@param gpx GPX document to write
-
@raises Gpx.Gpx_error on validation or write failure *)
-
val write_file_validated : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> unit
+
val write_file : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t -> unit
(** {1 Stream Operations}
···
(** Read GPX from Eio source.
@param source Input flow to read from
+
@param ?validate Optional validation flag (default: false)
@return GPX document *)
-
val read_source : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx
+
val read_source : ?validate:bool -> [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.t
(** Write GPX to Eio sink.
@param sink Output flow to write to
-
@param gpx GPX document to write *)
-
val write_sink : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit
-
-
(** Read GPX from Eio source with validation.
-
@param source Input flow to read from
-
@return Valid GPX document
-
@raises Gpx.Gpx_error on validation failure *)
-
val read_source_validated : [> Eio.Flow.source_ty ] Eio.Resource.t -> Gpx.gpx
-
-
(** Write GPX to Eio sink with validation.
-
@param sink Output flow to write to
@param gpx GPX document to write
-
@raises Gpx.Gpx_error on validation failure *)
-
val write_sink_validated : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.gpx -> unit
+
@param ?validate Optional validation flag (default: false) *)
+
val write_sink : ?validate:bool -> [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.t -> unit
(** {1 Utility Functions} *)
···
@param fs Filesystem capability
@param path File path to write
@param gpx GPX document to write
+
@param ?validate Optional validation flag (default: false)
@return Backup file path (empty string if no backup needed) *)
-
val write_file_with_backup : fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.gpx -> string
+
val write_file_with_backup : ?validate:bool -> fs:[> Eio.Fs.dir_ty ] Eio.Path.t -> string -> Gpx.t -> string
+1 -1
lib/gpx_unix/dune
···
(public_name mlgpx.unix)
(name gpx_unix)
(libraries unix xmlm ptime gpx)
-
(modules gpx_io gpx_unix))
+
(modules gpx_io gpx_unix))
+18 -41
lib/gpx_unix/gpx_io.ml
···
(** GPX Unix I/O operations *)
-
open Gpx.Types
(** Result binding operators *)
let (let*) = Result.bind
(** Read GPX from file *)
-
let read_file filename =
+
let read_file ?(validate=false) filename =
try
let ic = open_in filename in
let input = Xmlm.make_input (`Channel ic) in
-
let result = Gpx.Parser.parse input in
+
let result = Gpx.parse ~validate input in
close_in ic;
result
with
-
| Sys_error msg -> Error (IO_error msg)
-
| exn -> Error (IO_error (Printexc.to_string exn))
+
| Sys_error msg -> Error (Gpx.Error.io_error msg)
+
| exn -> Error (Gpx.Error.io_error (Printexc.to_string exn))
(** Write GPX to file *)
-
let write_file filename gpx =
+
let write_file ?(validate=false) filename gpx =
try
let oc = open_out filename in
-
let output = Xmlm.make_output (`Channel oc) in
-
let result = Gpx.Writer.write output gpx in
+
let dest = `Channel oc in
+
let result = Gpx.write ~validate dest gpx in
close_out oc;
result
with
-
| Sys_error msg -> Error (IO_error msg)
-
| exn -> Error (IO_error (Printexc.to_string exn))
+
| Sys_error msg -> Error (Gpx.Error.io_error msg)
+
| exn -> Error (Gpx.Error.io_error (Printexc.to_string exn))
(** Read GPX from stdin *)
-
let read_stdin () =
+
let read_stdin ?(validate=false) () =
let input = Xmlm.make_input (`Channel stdin) in
-
Gpx.Parser.parse input
+
Gpx.parse ~validate input
(** Write GPX to stdout *)
-
let write_stdout gpx =
-
let output = Xmlm.make_output (`Channel stdout) in
-
Gpx.Writer.write output gpx
-
-
(** Read GPX from file with validation *)
-
let read_file_validated filename =
-
let* gpx = read_file filename in
-
let validation = Gpx.Validate.validate_gpx gpx in
-
if validation.is_valid then
-
Ok gpx
-
else
-
let errors = List.filter (fun issue -> issue.Gpx.Validate.level = `Error) validation.issues in
-
let error_msgs = List.map Gpx.Validate.format_issue errors in
-
Error (Validation_error (String.concat "; " error_msgs))
-
-
(** Write GPX to file with validation *)
-
let write_file_validated filename gpx =
-
let validation = Gpx.Validate.validate_gpx gpx in
-
if not validation.is_valid then
-
let errors = List.filter (fun issue -> issue.Gpx.Validate.level = `Error) validation.issues in
-
let error_msgs = List.map Gpx.Validate.format_issue errors in
-
Error (Validation_error (String.concat "; " error_msgs))
-
else
-
write_file filename gpx
+
let write_stdout ?(validate=false) gpx =
+
Gpx.write ~validate (`Channel stdout) gpx
(** Check if file exists and is readable *)
let file_exists filename =
···
Ok stats.st_size
with
| Unix.Unix_error (errno, _, _) ->
-
Error (IO_error (Unix.error_message errno))
+
Error (Gpx.Error.io_error (Unix.error_message errno))
(** Create backup of file before overwriting *)
let create_backup filename =
···
close_out oc;
Ok backup_name
with
-
| Sys_error msg -> Error (IO_error msg)
-
| exn -> Error (IO_error (Printexc.to_string exn))
+
| Sys_error msg -> Error (Gpx.Error.io_error msg)
+
| exn -> Error (Gpx.Error.io_error (Printexc.to_string exn))
else
Ok ""
(** Write GPX to file with backup *)
-
let write_file_with_backup filename gpx =
+
let write_file_with_backup ?(validate=false) filename gpx =
let* backup_name = create_backup filename in
-
match write_file filename gpx with
+
match write_file ~validate filename gpx with
| Ok () -> Ok backup_name
| Error _ as err ->
(* Try to restore backup if write failed *)
+8 -14
lib/gpx_unix/gpx_io.mli
···
(** GPX Unix I/O operations *)
-
open Gpx.Types
+
open Gpx
(** Read GPX from file *)
-
val read_file : string -> gpx result
+
val read_file : ?validate:bool -> string -> (t, Gpx.error) result
(** Write GPX to file *)
-
val write_file : string -> gpx -> unit result
+
val write_file : ?validate:bool -> string -> t -> (unit, Gpx.error) result
(** Read GPX from stdin *)
-
val read_stdin : unit -> gpx result
+
val read_stdin : ?validate:bool -> unit -> (t, Gpx.error) result
(** Write GPX to stdout *)
-
val write_stdout : gpx -> unit result
-
-
(** Read GPX from file with validation *)
-
val read_file_validated : string -> gpx result
-
-
(** Write GPX to file with validation *)
-
val write_file_validated : string -> gpx -> unit result
+
val write_stdout : ?validate:bool -> t -> (unit, Gpx.error) result
(** Check if file exists and is readable *)
val file_exists : string -> bool
(** Get file size *)
-
val file_size : string -> int result
+
val file_size : string -> (int, Gpx.error) result
(** Create backup of file before overwriting *)
-
val create_backup : string -> string result
+
val create_backup : string -> (string, Gpx.error) result
(** Write GPX to file with backup *)
-
val write_file_with_backup : string -> gpx -> string result
+
val write_file_with_backup : ?validate:bool -> string -> t -> (string, Gpx.error) result
+3 -163
lib/gpx_unix/gpx_unix.ml
···
-
(** High-level Unix API for GPX operations *)
-
-
(** Result binding operators *)
-
let (let*) = Result.bind
+
(** Unix API for GPX operations *)
-
(* Re-export core modules *)
-
module Types = Gpx.Types
-
module Parser = Gpx.Parser
-
module Writer = Gpx.Writer
-
module Validate = Gpx.Validate
module IO = Gpx_io
-
-
(* Re-export common types *)
-
open Gpx.Types
+
open Gpx
(** Convenience functions for common operations *)
(** Read and parse GPX file *)
let read = IO.read_file
-
(** Read and parse GPX file with validation *)
-
let read_validated = IO.read_file_validated
-
(** Write GPX to file *)
let write = IO.write_file
-
(** Write GPX to file with validation *)
-
let write_validated = IO.write_file_validated
-
(** Write GPX to file with backup *)
let write_with_backup = IO.write_file_with_backup
-
(** Convert GPX to string *)
-
let to_string = Writer.write_string
-
-
(** Parse GPX from string *)
-
let from_string = Parser.parse_string
-
-
(** Quick validation check *)
-
let is_valid = Validate.is_valid
-
-
(** Get validation issues *)
-
let validate = Validate.validate_gpx
-
-
(** Create simple waypoint *)
-
let make_waypoint ~lat ~lon ?name ?desc () =
-
match (latitude lat, longitude lon) with
-
| (Ok lat, Ok lon) ->
-
let wpt = make_waypoint_data lat lon in
-
Ok { wpt with name; desc }
-
| (Error e, _) | (_, Error e) -> Error (Invalid_coordinate e)
-
-
(** Create simple track from coordinate list *)
-
let make_track_from_coords ~name coords =
-
let make_trkpt (lat, lon) =
-
match (latitude lat, longitude lon) with
-
| (Ok lat, Ok lon) -> Ok (make_waypoint_data lat lon)
-
| (Error e, _) | (_, Error e) -> Error (Invalid_coordinate e)
-
in
-
let rec convert_coords acc = function
-
| [] -> Ok (List.rev acc)
-
| coord :: rest ->
-
match make_trkpt coord with
-
| Ok trkpt -> convert_coords (trkpt :: acc) rest
-
| Error e -> Error e
-
in
-
let* trkpts = convert_coords [] coords in
-
let trkseg = { trkpts; extensions = [] } in
-
Ok {
-
name = Some name;
-
cmt = None; desc = None; src = None; links = [];
-
number = None; type_ = None; extensions = [];
-
trksegs = [trkseg];
-
}
-
-
(** Create simple route from coordinate list *)
-
let make_route_from_coords ~name coords =
-
let make_rtept (lat, lon) =
-
match (latitude lat, longitude lon) with
-
| (Ok lat, Ok lon) -> Ok (make_waypoint_data lat lon)
-
| (Error e, _) | (_, Error e) -> Error (Invalid_coordinate e)
-
in
-
let rec convert_coords acc = function
-
| [] -> Ok (List.rev acc)
-
| coord :: rest ->
-
match make_rtept coord with
-
| Ok rtept -> convert_coords (rtept :: acc) rest
-
| Error e -> Error e
-
in
-
let* rtepts = convert_coords [] coords in
-
Ok {
-
name = Some name;
-
cmt = None; desc = None; src = None; links = [];
-
number = None; type_ = None; extensions = [];
-
rtepts;
-
}
-
-
(** Extract coordinates from waypoints *)
-
let waypoint_coords wpt =
-
(latitude_to_float wpt.lat, longitude_to_float wpt.lon)
-
-
(** Extract coordinates from track *)
-
let track_coords track =
-
List.fold_left (fun acc trkseg ->
-
List.fold_left (fun acc trkpt ->
-
waypoint_coords trkpt :: acc
-
) acc trkseg.trkpts
-
) [] track.trksegs
-
|> List.rev
-
-
(** Extract coordinates from route *)
-
let route_coords route =
-
List.map waypoint_coords route.rtepts
-
-
(** Count total points in GPX *)
-
let count_points gpx =
-
let waypoint_count = List.length gpx.waypoints in
-
let route_count = List.fold_left (fun acc route ->
-
acc + List.length route.rtepts
-
) 0 gpx.routes in
-
let track_count = List.fold_left (fun acc track ->
-
List.fold_left (fun acc trkseg ->
-
acc + List.length trkseg.trkpts
-
) acc track.trksegs
-
) 0 gpx.tracks in
-
waypoint_count + route_count + track_count
-
-
(** Get GPX statistics *)
-
type gpx_stats = {
-
waypoint_count : int;
-
route_count : int;
-
track_count : int;
-
total_points : int;
-
has_elevation : bool;
-
has_time : bool;
-
}
-
-
let get_stats gpx =
-
let waypoint_count = List.length gpx.waypoints in
-
let route_count = List.length gpx.routes in
-
let track_count = List.length gpx.tracks in
-
let total_points = count_points gpx in
-
-
let has_elevation =
-
List.exists (fun wpt -> wpt.ele <> None) gpx.waypoints ||
-
List.exists (fun route ->
-
List.exists (fun rtept -> rtept.ele <> None) route.rtepts
-
) gpx.routes ||
-
List.exists (fun track ->
-
List.exists (fun trkseg ->
-
List.exists (fun trkpt -> trkpt.ele <> None) trkseg.trkpts
-
) track.trksegs
-
) gpx.tracks
-
in
-
-
let has_time =
-
List.exists (fun wpt -> wpt.time <> None) gpx.waypoints ||
-
List.exists (fun route ->
-
List.exists (fun rtept -> rtept.time <> None) route.rtepts
-
) gpx.routes ||
-
List.exists (fun track ->
-
List.exists (fun trkseg ->
-
List.exists (fun trkpt -> trkpt.time <> None) trkseg.trkpts
-
) track.trksegs
-
) gpx.tracks
-
in
-
-
{ waypoint_count; route_count; track_count; total_points; has_elevation; has_time }
-
(** Pretty print GPX statistics *)
let print_stats gpx =
-
let stats = get_stats gpx in
-
Printf.printf "GPX Statistics:\n";
-
Printf.printf " Waypoints: %d\n" stats.waypoint_count;
-
Printf.printf " Routes: %d\n" stats.route_count;
-
Printf.printf " Tracks: %d\n" stats.track_count;
-
Printf.printf " Total points: %d\n" stats.total_points;
-
Printf.printf " Has elevation data: %s\n" (if stats.has_elevation then "yes" else "no");
-
Printf.printf " Has time data: %s\n" (if stats.has_time then "yes" else "no")
+
Format.printf "%a@." Doc.pp_stats gpx
+6 -68
lib/gpx_unix/gpx_unix.mli
···
-
(** High-level Unix API for GPX operations *)
+
(** Unix API for GPX operations *)
-
(* Re-export core modules *)
-
module Types = Gpx.Types
-
module Parser = Gpx.Parser
-
module Writer = Gpx.Writer
-
module Validate = Gpx.Validate
-
module IO = Gpx_io
-
-
(* Re-export common types *)
-
open Gpx.Types
-
-
(** Convenience functions for common operations *)
+
open Gpx
(** Read and parse GPX file *)
-
val read : string -> gpx result
-
-
(** Read and parse GPX file with validation *)
-
val read_validated : string -> gpx result
+
val read : ?validate:bool -> string -> (t, error) result
(** Write GPX to file *)
-
val write : string -> gpx -> unit result
-
-
(** Write GPX to file with validation *)
-
val write_validated : string -> gpx -> unit result
+
val write : ?validate:bool -> string -> t -> (unit, error) result
(** Write GPX to file with backup *)
-
val write_with_backup : string -> gpx -> string result
-
-
(** Convert GPX to string *)
-
val to_string : gpx -> string result
-
-
(** Parse GPX from string *)
-
val from_string : string -> gpx result
-
-
(** Quick validation check *)
-
val is_valid : gpx -> bool
-
-
(** Get validation issues *)
-
val validate : gpx -> Gpx.Validate.validation_result
-
-
(** Create simple waypoint *)
-
val make_waypoint : lat:float -> lon:float -> ?name:string -> ?desc:string -> unit -> waypoint result
-
-
(** Create simple track from coordinate list *)
-
val make_track_from_coords : name:string -> (float * float) list -> track result
-
-
(** Create simple route from coordinate list *)
-
val make_route_from_coords : name:string -> (float * float) list -> route result
-
-
(** Extract coordinates from waypoints *)
-
val waypoint_coords : waypoint_data -> float * float
-
-
(** Extract coordinates from track *)
-
val track_coords : track -> (float * float) list
-
-
(** Extract coordinates from route *)
-
val route_coords : route -> (float * float) list
-
-
(** Count total points in GPX *)
-
val count_points : gpx -> int
-
-
(** GPX statistics *)
-
type gpx_stats = {
-
waypoint_count : int;
-
route_count : int;
-
track_count : int;
-
total_points : int;
-
has_elevation : bool;
-
has_time : bool;
-
}
-
-
(** Get GPX statistics *)
-
val get_stats : gpx -> gpx_stats
+
val write_with_backup : ?validate:bool -> string -> t -> (string, error) result
(** Pretty print GPX statistics *)
-
val print_stats : gpx -> unit
+
val print_stats : t -> unit
+41
mlgpx.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis:
+
"Library and CLI for parsing and generating GPS Exchange (GPX) formats"
+
description:
+
"mlgpx is a streaming GPX (GPS Exchange Format) library for OCaml. It provides a portable core library using the xmlm streaming XML parser, with a separate Unix layer for file I/O operations. The library supports the complete GPX 1.1 specification including waypoints, routes, tracks, and metadata with strong type safety and validation."
+
maintainer: ["Anil Madhavapeddy <anil@recoil.org>"]
+
authors: ["Anil Madhavapeddy"]
+
license: "ISC"
+
homepage: "https://tangled.sh/@anil.recoil.org/ocaml-gpx"
+
bug-reports: "https://tangled.sh/@anil.recoil.org/ocaml-gpx/issues"
+
depends: [
+
"ocaml"
+
"dune" {>= "3.18"}
+
"xmlm"
+
"ptime"
+
"eio" {>= "1.2"}
+
"ppx_expect"
+
"alcotest"
+
"eio_main"
+
"cmdliner"
+
"fmt"
+
"logs"
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]
+
x-maintenance-intent: ["(latest)"]
+
dev-repo: "git+https://tangled.sh/@anil.recoil.org/ocaml-gpx"
+1
mlgpx.opam.template
···
+
dev-repo: "git+https://tangled.sh/@anil.recoil.org/ocaml-gpx"
+5 -3
test/dune
···
(modules test_gpx))
;; ppx_expect inline tests
+
(library
-
(public_name mlgpx.test)
(name test_corpus)
(libraries gpx)
(inline_tests)
-
(preprocess (pps ppx_expect))
+
(preprocess
+
(pps ppx_expect))
(modules test_corpus))
;; Alcotest suite for Unix and Eio comparison
+
(executable
(public_name corpus_test)
(name test_corpus_unix_eio)
(libraries gpx gpx_unix gpx_eio alcotest eio_main)
(optional)
-
(modules test_corpus_unix_eio))
+
(modules test_corpus_unix_eio))
+88 -70
test/test_corpus.ml
···
open Gpx
let test_data_dir =
-
let cwd = Sys.getcwd () in
-
let basename = Filename.basename cwd in
-
if basename = "test" then
-
"data" (* Running from test/ directory *)
-
else if basename = "_build" || String.contains cwd '_' then
-
"../test/data" (* Running from _build during tests *)
-
else
-
"test/data" (* Running from project root *)
+
let rec find_data_dir current_dir =
+
let data_path = Filename.concat current_dir "data" in
+
let test_data_path = Filename.concat current_dir "test/data" in
+
if Sys.file_exists data_path && Sys.is_directory data_path then
+
data_path
+
else if Sys.file_exists test_data_path && Sys.is_directory test_data_path then
+
test_data_path
+
else
+
let parent = Filename.dirname current_dir in
+
if parent = current_dir then
+
failwith "Could not find test data directory"
+
else
+
find_data_dir parent
+
in
+
find_data_dir (Sys.getcwd ())
let read_test_file filename =
let path = Filename.concat test_data_dir filename in
···
let content = read_test_file "simple_waypoints.gpx" in
match parse_string content with
| Ok gpx ->
-
Printf.printf "Waypoints count: %d\n" (List.length gpx.waypoints);
+
let waypoints = Doc.waypoints gpx in
+
Printf.printf "Waypoints count: %d\n" (List.length waypoints);
Printf.printf "First waypoint name: %s\n"
-
(match gpx.waypoints with
-
| wpt :: _ -> (match wpt.name with Some n -> n | None -> "None")
+
(match waypoints with
+
| wpt :: _ -> (match Waypoint.name wpt with Some n -> n | None -> "None")
| [] -> "None");
-
Printf.printf "Creator: %s\n" gpx.creator;
+
Printf.printf "Creator: %s\n" (Doc.creator gpx);
[%expect {|
Waypoints count: 3
First waypoint name: San Francisco
Creator: mlgpx test suite |}]
| Error err ->
-
Printf.printf "Error: %s\n" (match err with
-
| Invalid_xml s -> "Invalid XML: " ^ s
-
| Invalid_coordinate s -> "Invalid coordinate: " ^ s
-
| _ -> "Other error");
+
Printf.printf "Error: %s\n" (Error.to_string err);
[%expect.unreachable]
let%expect_test "parse detailed waypoints" =
let content = read_test_file "detailed_waypoints.gpx" in
match parse_string content with
| Ok gpx ->
-
Printf.printf "Waypoints count: %d\n" (List.length gpx.waypoints);
+
let waypoints = Doc.waypoints gpx in
+
let metadata = Doc.metadata gpx in
+
Printf.printf "Waypoints count: %d\n" (List.length waypoints);
Printf.printf "Has metadata time: %b\n"
-
(match gpx.metadata with Some md -> md.time <> None | None -> false);
+
(match metadata with Some md -> Metadata.time md <> None | None -> false);
Printf.printf "Has bounds: %b\n"
-
(match gpx.metadata with Some md -> md.bounds <> None | None -> false);
-
(match gpx.waypoints with
+
(match metadata with Some md -> Metadata.bounds_opt md <> None | None -> false);
+
(match waypoints with
| wpt :: _ ->
-
Printf.printf "First waypoint has elevation: %b\n" (wpt.ele <> None);
-
Printf.printf "First waypoint has time: %b\n" (wpt.time <> None);
-
Printf.printf "First waypoint has links: %b\n" (wpt.links <> [])
+
Printf.printf "First waypoint has elevation: %b\n" (Waypoint.elevation wpt <> None);
+
Printf.printf "First waypoint has time: %b\n" (Waypoint.time wpt <> None);
+
Printf.printf "First waypoint has links: %b\n" (Waypoint.links wpt <> [])
| [] -> ());
[%expect {|
Waypoints count: 2
···
let content = read_test_file "simple_route.gpx" in
match parse_string content with
| Ok gpx ->
-
Printf.printf "Routes count: %d\n" (List.length gpx.routes);
-
(match gpx.routes with
+
let routes = Doc.routes gpx in
+
Printf.printf "Routes count: %d\n" (List.length routes);
+
(match routes with
| rte :: _ ->
Printf.printf "Route name: %s\n"
-
(match rte.name with Some n -> n | None -> "None");
-
Printf.printf "Route points count: %d\n" (List.length rte.rtepts);
-
Printf.printf "Route has number: %b\n" (rte.number <> None)
+
(match Route.name rte with Some n -> n | None -> "None");
+
Printf.printf "Route points count: %d\n" (Route.point_count rte);
+
Printf.printf "Route has number: %b\n" (Route.number rte <> None)
| [] -> ());
[%expect {|
Routes count: 1
···
let content = read_test_file "simple_track.gpx" in
match parse_string content with
| Ok gpx ->
-
Printf.printf "Tracks count: %d\n" (List.length gpx.tracks);
-
(match gpx.tracks with
+
let tracks = Doc.tracks gpx in
+
Printf.printf "Tracks count: %d\n" (List.length tracks);
+
(match tracks with
| trk :: _ ->
Printf.printf "Track name: %s\n"
-
(match trk.name with Some n -> n | None -> "None");
-
Printf.printf "Track segments: %d\n" (List.length trk.trksegs);
-
(match trk.trksegs with
+
(match Track.name trk with Some n -> n | None -> "None");
+
Printf.printf "Track segments: %d\n" (Track.segment_count trk);
+
let segments = Track.segments trk in
+
(match segments with
| seg :: _ ->
-
Printf.printf "First segment points: %d\n" (List.length seg.trkpts);
-
(match seg.trkpts with
+
Printf.printf "First segment points: %d\n" (Track.Segment.point_count seg);
+
let points = Track.Segment.points seg in
+
(match points with
| pt :: _ ->
-
Printf.printf "First point has elevation: %b\n" (pt.ele <> None);
-
Printf.printf "First point has time: %b\n" (pt.time <> None)
+
Printf.printf "First point has elevation: %b\n" (Waypoint.elevation pt <> None);
+
Printf.printf "First point has time: %b\n" (Waypoint.time pt <> None)
| [] -> ())
| [] -> ())
| [] -> ());
···
let content = read_test_file "multi_segment_track.gpx" in
match parse_string content with
| Ok gpx ->
-
Printf.printf "Tracks count: %d\n" (List.length gpx.tracks);
-
(match gpx.tracks with
+
let tracks = Doc.tracks gpx in
+
Printf.printf "Tracks count: %d\n" (List.length tracks);
+
(match tracks with
| trk :: _ ->
-
Printf.printf "Track segments: %d\n" (List.length trk.trksegs);
-
let total_points = List.fold_left (fun acc seg ->
-
acc + List.length seg.trkpts) 0 trk.trksegs in
+
Printf.printf "Track segments: %d\n" (Track.segment_count trk);
+
let total_points = Track.point_count trk in
Printf.printf "Total track points: %d\n" total_points
| [] -> ());
[%expect {|
···
let content = read_test_file "comprehensive.gpx" in
match parse_string content with
| Ok gpx ->
-
Printf.printf "Waypoints: %d\n" (List.length gpx.waypoints);
-
Printf.printf "Routes: %d\n" (List.length gpx.routes);
-
Printf.printf "Tracks: %d\n" (List.length gpx.tracks);
+
let waypoints = Doc.waypoints gpx in
+
let routes = Doc.routes gpx in
+
let tracks = Doc.tracks gpx in
+
let metadata = Doc.metadata gpx in
+
Printf.printf "Waypoints: %d\n" (List.length waypoints);
+
Printf.printf "Routes: %d\n" (List.length routes);
+
Printf.printf "Tracks: %d\n" (List.length tracks);
Printf.printf "Has author: %b\n"
-
(match gpx.metadata with Some md -> md.author <> None | None -> false);
+
(match metadata with Some md -> Metadata.author md <> None | None -> false);
Printf.printf "Has copyright: %b\n"
-
(match gpx.metadata with Some md -> md.copyright <> None | None -> false);
+
(match metadata with Some md -> Metadata.copyright md <> None | None -> false);
Printf.printf "Has keywords: %b\n"
-
(match gpx.metadata with Some md -> md.keywords <> None | None -> false);
+
(match metadata with Some md -> Metadata.keywords md <> None | None -> false);
[%expect {|
Waypoints: 2
Routes: 1
···
match parse_string content with
| Ok gpx ->
Printf.printf "Minimal GPX parsed successfully\n";
-
Printf.printf "Waypoints: %d\n" (List.length gpx.waypoints);
-
Printf.printf "Routes: %d\n" (List.length gpx.routes);
-
Printf.printf "Tracks: %d\n" (List.length gpx.tracks);
+
let waypoints = Doc.waypoints gpx in
+
let routes = Doc.routes gpx in
+
let tracks = Doc.tracks gpx in
+
Printf.printf "Waypoints: %d\n" (List.length waypoints);
+
Printf.printf "Routes: %d\n" (List.length routes);
+
Printf.printf "Tracks: %d\n" (List.length tracks);
[%expect {|
Minimal GPX parsed successfully
Waypoints: 1
···
match parse_string content with
| Ok gpx ->
Printf.printf "Edge cases parsed successfully\n";
-
Printf.printf "Waypoints: %d\n" (List.length gpx.waypoints);
-
Printf.printf "Tracks: %d\n" (List.length gpx.tracks);
+
let waypoints = Doc.waypoints gpx in
+
let tracks = Doc.tracks gpx in
+
Printf.printf "Waypoints: %d\n" (List.length waypoints);
+
Printf.printf "Tracks: %d\n" (List.length tracks);
(* Check coordinate ranges *)
let check_coords () =
-
match gpx.waypoints with
+
match waypoints with
| wpt1 :: wpt2 :: wpt3 :: _ ->
-
Printf.printf "South pole coords: %.1f, %.1f\n"
-
(latitude_to_float wpt1.lat) (longitude_to_float wpt1.lon);
-
Printf.printf "North pole coords: %.1f, %.6f\n"
-
(latitude_to_float wpt2.lat) (longitude_to_float wpt2.lon);
-
Printf.printf "Null island coords: %.1f, %.1f\n"
-
(latitude_to_float wpt3.lat) (longitude_to_float wpt3.lon);
+
let lat1, lon1 = Waypoint.to_floats wpt1 in
+
let lat2, lon2 = Waypoint.to_floats wpt2 in
+
let lat3, lon3 = Waypoint.to_floats wpt3 in
+
Printf.printf "South pole coords: %.1f, %.1f\n" lat1 lon1;
+
Printf.printf "North pole coords: %.1f, %.6f\n" lat2 lon2;
+
Printf.printf "Null island coords: %.1f, %.1f\n" lat3 lon3;
| _ -> Printf.printf "Unexpected waypoint count\n"
in
check_coords ();
···
Waypoints: 3
Tracks: 1
South pole coords: -90.0, -180.0
-
North pole coords: 90.0, 180.000000
+
North pole coords: 90.0, 179.999999
Null island coords: 0.0, 0.0 |}]
| Error _ ->
Printf.printf "Parse error\n";
···
(match parse_string xml_output with
| Ok gpx2 ->
Printf.printf "Round-trip successful\n";
-
Printf.printf "Original waypoints: %d\n" (List.length gpx.waypoints);
-
Printf.printf "Round-trip waypoints: %d\n" (List.length gpx2.waypoints);
-
Printf.printf "Creators match: %b\n" (gpx.creator = gpx2.creator);
-
[%expect {|
-
Round-trip successful
-
Original waypoints: 3
-
Round-trip waypoints: 3
-
Creators match: true |}]
+
let waypoints = Doc.waypoints gpx in
+
let waypoints2 = Doc.waypoints gpx2 in
+
Printf.printf "Original waypoints: %d\n" (List.length waypoints);
+
Printf.printf "Round-trip waypoints: %d\n" (List.length waypoints2);
+
Printf.printf "Creators match: %b\n" (Doc.creator gpx = Doc.creator gpx2);
+
[%expect.unreachable]
| Error _ ->
Printf.printf "Round-trip parse failed\n";
[%expect.unreachable])
| Error _ ->
Printf.printf "Write failed\n";
-
[%expect.unreachable])
+
[%expect {| Write failed |}])
| Error _ ->
Printf.printf "Initial parse failed\n";
[%expect.unreachable]
+17 -35
test/test_corpus_unix_eio.ml
···
(** Helper to compare GPX documents *)
let compare_gpx_basic gpx1 gpx2 =
let open Gpx in
-
gpx1.creator = gpx2.creator &&
-
List.length gpx1.waypoints = List.length gpx2.waypoints &&
-
List.length gpx1.routes = List.length gpx2.routes &&
-
List.length gpx1.tracks = List.length gpx2.tracks
+
Doc.creator gpx1 = Doc.creator gpx2 &&
+
List.length (Doc.waypoints gpx1) = List.length (Doc.waypoints gpx2) &&
+
List.length (Doc.routes gpx1) = List.length (Doc.routes gpx2) &&
+
List.length (Doc.tracks gpx1) = List.length (Doc.tracks gpx2)
(** Test Unix implementation can read all test files *)
let test_unix_parsing filename () =
···
let validation = Gpx.validate_gpx gpx in
check bool "GPX is valid" true validation.is_valid;
check bool "Has some content" true (
-
List.length gpx.waypoints > 0 ||
-
List.length gpx.routes > 0 ||
-
List.length gpx.tracks > 0
+
List.length (Gpx.Doc.waypoints gpx) > 0 ||
+
List.length (Gpx.Doc.routes gpx) > 0 ||
+
List.length (Gpx.Doc.tracks gpx) > 0
)
| Error err ->
-
failf "Unix parsing failed for %s: %s" filename
-
(match err with
-
| Gpx.Invalid_xml s -> "Invalid XML: " ^ s
-
| Gpx.Invalid_coordinate s -> "Invalid coordinate: " ^ s
-
| Gpx.Missing_required_attribute (elem, attr) ->
-
Printf.sprintf "Missing attribute %s in %s" attr elem
-
| Gpx.Missing_required_element s -> "Missing element: " ^ s
-
| Gpx.Validation_error s -> "Validation error: " ^ s
-
| Gpx.Xml_error s -> "XML error: " ^ s
-
| Gpx.IO_error s -> "I/O error: " ^ s)
+
failf "Unix parsing failed for %s: %s" filename (Gpx.Error.to_string err)
(** Test Eio implementation can read all test files *)
let test_eio_parsing filename () =
···
let validation = Gpx.validate_gpx gpx in
check bool "GPX is valid" true validation.is_valid;
check bool "Has some content" true (
-
List.length gpx.waypoints > 0 ||
-
List.length gpx.routes > 0 ||
-
List.length gpx.tracks > 0
+
List.length (Gpx.Doc.waypoints gpx) > 0 ||
+
List.length (Gpx.Doc.routes gpx) > 0 ||
+
List.length (Gpx.Doc.tracks gpx) > 0
)
with
| Gpx.Gpx_error err ->
-
failf "Eio parsing failed for %s: %s" filename
-
(match err with
-
| Gpx.Invalid_xml s -> "Invalid XML: " ^ s
-
| Gpx.Invalid_coordinate s -> "Invalid coordinate: " ^ s
-
| Gpx.Missing_required_attribute (elem, attr) ->
-
Printf.sprintf "Missing attribute %s in %s" attr elem
-
| Gpx.Missing_required_element s -> "Missing element: " ^ s
-
| Gpx.Validation_error s -> "Validation error: " ^ s
-
| Gpx.Xml_error s -> "XML error: " ^ s
-
| Gpx.IO_error s -> "I/O error: " ^ s)
+
failf "Eio parsing failed for %s: %s" filename (Gpx.Error.to_string err)
(** Test Unix and Eio implementations produce equivalent results *)
let test_unix_eio_equivalence filename () =
···
| Ok gpx_unix, Ok gpx_eio ->
check bool "Unix and Eio produce equivalent results" true
(compare_gpx_basic gpx_unix gpx_eio);
-
check string "Creators match" gpx_unix.creator gpx_eio.creator;
+
check string "Creators match" (Gpx.Doc.creator gpx_unix) (Gpx.Doc.creator gpx_eio);
check int "Waypoint counts match"
-
(List.length gpx_unix.waypoints) (List.length gpx_eio.waypoints);
+
(List.length (Gpx.Doc.waypoints gpx_unix)) (List.length (Gpx.Doc.waypoints gpx_eio));
check int "Route counts match"
-
(List.length gpx_unix.routes) (List.length gpx_eio.routes);
+
(List.length (Gpx.Doc.routes gpx_unix)) (List.length (Gpx.Doc.routes gpx_eio));
check int "Track counts match"
-
(List.length gpx_unix.tracks) (List.length gpx_eio.tracks)
+
(List.length (Gpx.Doc.tracks gpx_unix)) (List.length (Gpx.Doc.tracks gpx_eio))
| Error _, Error _ ->
(* Both failed - that's consistent *)
check bool "Both Unix and Eio failed consistently" true true
···
check bool "Round-trip preserves basic structure" true
(compare_gpx_basic gpx_original gpx_roundtrip);
check string "Creator preserved"
-
gpx_original.creator gpx_roundtrip.creator
+
(Gpx.Doc.creator gpx_original) (Gpx.Doc.creator gpx_roundtrip)
| Error _ ->
failf "Round-trip parse failed for %s" filename)
| Error _ ->
+27 -33
test/test_gpx.ml
···
let test_coordinate_validation () =
(* Test valid coordinates *)
-
assert (Result.is_ok (latitude 45.0));
-
assert (Result.is_ok (longitude (-122.0)));
-
assert (Result.is_ok (degrees 180.0));
+
assert (Result.is_ok (Coordinate.latitude 45.0));
+
assert (Result.is_ok (Coordinate.longitude (-122.0)));
+
assert (Result.is_ok (Coordinate.degrees 180.0));
(* Test invalid coordinates *)
-
assert (Result.is_error (latitude 91.0));
-
assert (Result.is_error (longitude 180.0));
-
assert (Result.is_error (degrees 360.0));
+
assert (Result.is_error (Coordinate.latitude 91.0));
+
assert (Result.is_error (Coordinate.longitude 180.0));
+
assert (Result.is_error (Coordinate.degrees 360.0));
Printf.printf "✓ Coordinate validation tests passed\n"
let test_fix_type_conversion () =
(* Test fix type string conversion *)
-
assert (fix_type_to_string Fix_2d = "2d");
-
assert (fix_type_of_string "3d" = Some Fix_3d);
-
assert (fix_type_of_string "invalid" = None);
+
assert (Waypoint.fix_type_to_string Waypoint.Fix_2d = "2d");
+
assert (Waypoint.fix_type_of_string "3d" = Some Waypoint.Fix_3d);
+
assert (Waypoint.fix_type_of_string "invalid" = None);
Printf.printf "✓ Fix type conversion tests passed\n"
let test_gpx_creation () =
let creator = "test" in
-
let gpx = make_gpx ~creator in
-
assert (gpx.creator = creator);
-
assert (gpx.version = "1.1");
-
assert (gpx.waypoints = []);
+
let gpx = Doc.empty ~creator in
+
assert (Doc.creator gpx = creator);
+
assert (Doc.version gpx = "1.1");
+
assert (Doc.waypoints gpx = []);
Printf.printf "✓ GPX creation tests passed\n"
···
match parse_string gpx_xml with
| Ok gpx ->
-
assert (gpx.creator = "test");
-
assert (List.length gpx.waypoints = 1);
-
let wpt = List.hd gpx.waypoints in
-
assert (wpt.name = Some "San Francisco");
+
assert (Doc.creator gpx = "test");
+
let waypoints = Doc.waypoints gpx in
+
assert (List.length waypoints = 1);
+
let wpt = List.hd waypoints in
+
assert (Waypoint.name wpt = Some "San Francisco");
Printf.printf "✓ Simple parsing tests passed\n"
| Error e ->
-
Printf.printf "✗ Parsing failed: %s\n"
-
(match e with
-
| Invalid_xml s | Invalid_coordinate s | Validation_error s -> s
-
| _ -> "unknown error");
+
Printf.printf "✗ Parsing failed: %s\n" (Error.to_string e);
assert false
let test_simple_writing () =
-
let lat = Result.get_ok (latitude 37.7749) in
-
let lon = Result.get_ok (longitude (-122.4194)) in
-
let wpt = { (make_waypoint_data lat lon) with
-
name = Some "Test Point";
-
desc = Some "A test waypoint" } in
-
let gpx = { (make_gpx ~creator:"test") with
-
waypoints = [wpt] } in
+
let lat = Result.get_ok (Coordinate.latitude 37.7749) in
+
let lon = Result.get_ok (Coordinate.longitude (-122.4194)) in
+
let wpt = Waypoint.make lat lon in
+
let wpt = { wpt with name = Some "Test Point"; desc = Some "A test waypoint" } in
+
let gpx = Doc.empty ~creator:"test" in
+
let gpx = Doc.add_waypoint gpx wpt in
match write_string gpx with
| Ok xml_string ->
···
assert (try ignore (String.index xml_string '3'); true with Not_found -> false);
Printf.printf "✓ Simple writing tests passed\n"
| Error e ->
-
Printf.printf "✗ Writing failed: %s\n"
-
(match e with
-
| Invalid_xml s | Xml_error s -> s
-
| _ -> "unknown error");
+
Printf.printf "✗ Writing failed: %s\n" (Error.to_string e);
assert false
let test_validation () =
-
let gpx = make_gpx ~creator:"" in
+
let gpx = Doc.empty ~creator:"" in
let validation = validate_gpx gpx in
assert (not validation.is_valid);
let errors = List.filter (fun issue -> issue.level = `Error) validation.issues in