GPS Exchange Format library/CLI in OCaml

Compare changes

Choose any two refs to compare.

+32 -11
.tangled/workflows/build.yml
···
dependencies:
nixpkgs:
- shell
+
- stdenv
+
- findutils
+
- binutils
+
- libunwind
+
- ncurses
+
- opam
+
- git
+
- gawk
+
- gnupatch
+
- gnum4
+
- gnumake
+
- gnutar
+
- gnused
+
- gnugrep
+
- diffutils
+
- gzip
+
- bzip2
- gcc
-
- dune_3
- ocaml
-
- ocamlpackages.xmlm
-
- ocamlpackages.alcotest
-
- ocamlpackages.eio
-
- ocamlpackages.fmt
-
- ocamlpackages.eio_main
-
- ocamlpackages.ppx_expect
-
- ocamlpackages.cmdliner
-
- ocamlpackages.ptime
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: |
-
dune build
+
opam exec -- dune runtest --verbose
+
- name: doc
+
command: |
+
opam install -y odoc
+
opam exec -- dune build @doc
+1 -1
CHANGES.md
···
-
# dev
+
# v1.0.0
- Initial public release
+2 -4
README.md
···
dune build @install
dune install
-
# Or use opam (when published)
+
# Or use opam
opam install mlgpx
```
···
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 ()
```
-
-
+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))
+450 -1
bin/mlgpx_cli.ml
···
-
(* Temporarily disabled during refactoring *)
+
(** mlgpx Command Line Interface with pretty ANSI output *)
+
+
open Cmdliner
+
open Gpx
+
+
(* Terminal and formatting setup *)
+
let setup_fmt style_renderer =
+
Fmt_tty.setup_std_outputs ?style_renderer ();
+
()
+
+
(* Color formatters *)
+
let info_style = Fmt.(styled (`Fg `Green))
+
let warn_style = Fmt.(styled (`Fg `Yellow))
+
let error_style = Fmt.(styled (`Fg `Red))
+
let success_style = Fmt.(styled (`Fg `Green))
+
let bold_style = Fmt.(styled `Bold)
+
+
(* Logging functions *)
+
let log_info fmt =
+
Fmt.pf Format.err_formatter "[%a] " (info_style Fmt.string) "INFO";
+
Format.kfprintf (fun fmt -> Format.pp_print_newline fmt (); Format.pp_print_flush fmt ()) Format.err_formatter fmt
+
+
+
let log_error fmt =
+
Fmt.pf Format.err_formatter "[%a] " (error_style Fmt.string) "ERROR";
+
Format.kfprintf (fun fmt -> Format.pp_print_newline fmt (); Format.pp_print_flush fmt ()) Format.err_formatter fmt
+
+
let log_success fmt =
+
Format.kfprintf (fun fmt -> Format.pp_print_newline fmt (); Format.pp_print_flush fmt ()) Format.std_formatter fmt
+
+
(* Utility functions *)
+
let waypoints_to_track_segments waypoints =
+
if waypoints = [] then
+
[]
+
else
+
Track.Segment.make waypoints :: []
+
+
let sort_waypoints sort_by_time sort_by_name waypoints =
+
if sort_by_time then
+
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 wpt2 ->
+
match Waypoint.name wpt1, Waypoint.name wpt2 with
+
| Some n1, Some n2 -> String.compare n1 n2
+
| Some _, None -> -1
+
| None, Some _ -> 1
+
| None, None -> 0
+
) waypoints
+
else
+
waypoints
+
+
(* Main conversion command *)
+
let convert_waypoints_to_trackset input_file output_file track_name track_desc
+
sort_by_time sort_by_name preserve_waypoints verbose style_renderer =
+
setup_fmt style_renderer;
+
let run env =
+
try
+
let fs = Eio.Stdenv.fs env in
+
+
if verbose then
+
log_info "Reading GPX file: %a" (bold_style Fmt.string) input_file;
+
+
(* Read input GPX *)
+
let gpx = Gpx_eio.read ~fs input_file in
+
+
if verbose then
+
log_info "Found %d waypoints and %d existing tracks"
+
(Doc.waypoint_count gpx)
+
(Doc.track_count gpx);
+
+
(* Check if we have waypoints to convert *)
+
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 (Doc.waypoints gpx) in
+
+
if verbose && (sort_by_time || sort_by_name) then
+
log_info "Sorted %d waypoints" (List.length sorted_waypoints);
+
+
(* Convert waypoints to track segments *)
+
let track_segments = waypoints_to_track_segments sorted_waypoints in
+
+
(* Create the new track *)
+
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";
+
type_ = Some "converted";
+
trksegs = track_segments;
+
} in
+
+
if verbose then (
+
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 =
+
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" 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 = Gpx.validate_gpx output_gpx in
+
if not validation.is_valid then (
+
log_error "Generated GPX failed validation:";
+
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
+
) validation.issues;
+
exit 1
+
);
+
+
if verbose then
+
log_info "Writing output to: %a" (bold_style Fmt.string) output_file;
+
+
(* Write output GPX *)
+
Gpx_eio.write ~fs output_file output_gpx;
+
+
if verbose then (
+
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"
+
(Doc.waypoint_count output_gpx)
+
(if preserve_waypoints then " (preserved)" else " (removed)");
+
Fmt.pf Format.err_formatter " - %d tracks (%a + %d existing)\n"
+
(Doc.track_count output_gpx)
+
(success_style Fmt.string) "1 new"
+
(Doc.track_count gpx)
+
) else (
+
log_success "Converted %d waypoints to track: %a โ†’ %a"
+
(List.length sorted_waypoints)
+
(bold_style Fmt.string) input_file
+
(bold_style Fmt.string) output_file
+
)
+
+
with
+
| Gpx.Gpx_error err ->
+
log_error "GPX Error: %s" (Error.to_string err);
+
exit 2
+
| Sys_error msg ->
+
log_error "System error: %s" msg;
+
exit 2
+
| exn ->
+
log_error "Unexpected error: %s" (Printexc.to_string exn);
+
exit 2
+
in
+
Eio_main.run run
+
+
(* Helper function to collect all timestamps from GPX *)
+
let collect_all_timestamps gpx =
+
let times = ref [] in
+
+
(* Collect from waypoints *)
+
List.iter (fun wpt ->
+
match Waypoint.time wpt with
+
| Some t -> times := t :: !times
+
| None -> ()
+
) (Doc.waypoints gpx);
+
+
(* Collect from routes *)
+
List.iter (fun route ->
+
List.iter (fun rtept ->
+
match Waypoint.time rtept with
+
| Some t -> times := t :: !times
+
| None -> ()
+
) (Route.points route)
+
) (Doc.routes gpx);
+
+
(* Collect from tracks *)
+
List.iter (fun track ->
+
List.iter (fun seg ->
+
List.iter (fun trkpt ->
+
match Waypoint.time trkpt with
+
| Some t -> times := t :: !times
+
| None -> ()
+
) (Track.Segment.points seg)
+
) (Track.segments track)
+
) (Doc.tracks gpx);
+
+
!times
+
+
(* Info command *)
+
let info_command input_file verbose style_renderer =
+
setup_fmt style_renderer;
+
let run env =
+
try
+
let fs = Eio.Stdenv.fs env in
+
+
if verbose then
+
log_info "Analyzing GPX file: %a" (bold_style Fmt.string) input_file;
+
+
let gpx = Gpx_eio.read ~fs input_file in
+
+
(* Header *)
+
Fmt.pf Format.std_formatter "%a\n" (bold_style Fmt.string) "GPX File Information";
+
+
(* Basic info *)
+
Printf.printf " Version: %s\n" (Doc.version gpx);
+
Printf.printf " Creator: %s\n" (Doc.creator gpx);
+
+
(match Doc.metadata gpx with
+
| Some meta ->
+
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 ->
+
Printf.printf " No metadata\n");
+
+
(* Content summary *)
+
Fmt.pf Format.std_formatter "\n%a\n" (bold_style Fmt.string) "Content Summary";
+
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
+
if all_times <> [] then (
+
let sorted_times = List.sort Ptime.compare all_times in
+
let start_time = List.hd sorted_times in
+
let stop_time = List.hd (List.rev sorted_times) in
+
+
Fmt.pf Format.std_formatter "\n%a\n" (bold_style Fmt.string) "Time Range";
+
Fmt.pf Format.std_formatter " Start: %a\n" (info_style Fmt.string) (Ptime.to_rfc3339 start_time);
+
Fmt.pf Format.std_formatter " Stop: %a\n" (info_style Fmt.string) (Ptime.to_rfc3339 stop_time);
+
+
(* Calculate duration *)
+
let duration_span = Ptime.diff stop_time start_time in
+
match Ptime.Span.to_int_s duration_span with
+
| Some seconds ->
+
let days = seconds / 86400 in
+
let hours = (seconds mod 86400) / 3600 in
+
let minutes = (seconds mod 3600) / 60 in
+
+
if days > 0 then
+
Fmt.pf Format.std_formatter " Duration: %a\n" (bold_style Fmt.string)
+
(Printf.sprintf "%d days, %d hours, %d minutes" days hours minutes)
+
else if hours > 0 then
+
Fmt.pf Format.std_formatter " Duration: %a\n" (bold_style Fmt.string)
+
(Printf.sprintf "%d hours, %d minutes" hours minutes)
+
else
+
Fmt.pf Format.std_formatter " Duration: %a\n" (bold_style Fmt.string)
+
(Printf.sprintf "%d minutes" minutes)
+
| None ->
+
(* Duration too large to represent as int *)
+
Fmt.pf Format.std_formatter " Duration: %a\n" (bold_style Fmt.string)
+
(Printf.sprintf "%.1f days" (Ptime.Span.to_float_s duration_span /. 86400.));
+
+
Printf.printf " Total points with timestamps: %d\n" (List.length all_times)
+
);
+
+
(* Detailed waypoint info *)
+
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.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 (Doc.waypoints gpx) <= 10 then (
+
Printf.printf " Details:\n";
+
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 (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 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 = 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 track) ~default:"<unnamed>")
+
(Track.segment_count track) total_points
+
) (Doc.tracks gpx)
+
);
+
+
(* Validation *)
+
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 : 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
+
) validation.issues
+
)
+
+
with
+
| Gpx.Gpx_error err ->
+
log_error "GPX Error: %s" (Error.to_string err);
+
exit 2
+
| Sys_error msg ->
+
log_error "System error: %s" msg;
+
exit 2
+
| exn ->
+
log_error "Unexpected error: %s" (Printexc.to_string exn);
+
exit 2
+
in
+
Eio_main.run run
+
+
(* CLI argument definitions *)
+
let input_file_arg =
+
let doc = "Input GPX file path" in
+
Arg.(required & pos 0 (some non_dir_file) None & info [] ~docv:"INPUT" ~doc)
+
+
let output_file_arg =
+
let doc = "Output GPX file path" in
+
Arg.(required & pos 1 (some string) None & info [] ~docv:"OUTPUT" ~doc)
+
+
let track_name_opt =
+
let doc = "Name for the generated track (default: \"Converted from waypoints\")" in
+
Arg.(value & opt string "Converted from waypoints" & info ["n"; "name"] ~docv:"NAME" ~doc)
+
+
let track_description_opt =
+
let doc = "Description for the generated track" in
+
Arg.(value & opt (some string) None & info ["d"; "desc"] ~docv:"DESC" ~doc)
+
+
let sort_by_time_flag =
+
let doc = "Sort waypoints by timestamp before conversion" in
+
Arg.(value & flag & info ["t"; "sort-time"] ~doc)
+
+
let sort_by_name_flag =
+
let doc = "Sort waypoints by name before conversion" in
+
Arg.(value & flag & info ["sort-name"] ~doc)
+
+
let preserve_waypoints_flag =
+
let doc = "Keep original waypoints in addition to generated track" in
+
Arg.(value & flag & info ["p"; "preserve"] ~doc)
+
+
let verbose_flag =
+
let doc = "Enable verbose output" in
+
Arg.(value & flag & info ["v"; "verbose"] ~doc)
+
+
(* Command definitions *)
+
let convert_cmd =
+
let doc = "Convert waypoints to trackset" in
+
let man = [
+
`S Manpage.s_description;
+
`P "Convert all waypoints in a GPX file to a single track. This is useful for \
+
converting a collection of waypoints into a navigable route or for \
+
consolidating GPS data.";
+
`P "The conversion preserves all waypoint data (coordinates, elevation, \
+
timestamps, etc.) in the track points. By default, waypoints are removed \
+
from the output file unless --preserve is used.";
+
`S Manpage.s_examples;
+
`P "Convert waypoints to track:";
+
`Pre " mlgpx convert waypoints.gpx track.gpx";
+
`P "Convert with custom track name and preserve original waypoints:";
+
`Pre " mlgpx convert -n \"My Route\" --preserve waypoints.gpx route.gpx";
+
`P "Sort waypoints by timestamp before conversion:";
+
`Pre " mlgpx convert --sort-time waypoints.gpx sorted_track.gpx";
+
] in
+
let term = Term.(const convert_waypoints_to_trackset $ input_file_arg $ output_file_arg
+
$ track_name_opt $ track_description_opt $ sort_by_time_flag
+
$ sort_by_name_flag $ preserve_waypoints_flag $ verbose_flag
+
$ Fmt_cli.style_renderer ()) in
+
Cmd.v (Cmd.info "convert" ~doc ~man) term
+
+
let info_cmd =
+
let doc = "Display information about a GPX file" in
+
let man = [
+
`S Manpage.s_description;
+
`P "Analyze and display detailed information about a GPX file including \
+
statistics, content summary, and validation results.";
+
`P "This command is useful for understanding the structure and content \
+
of GPX files before processing them.";
+
`S Manpage.s_examples;
+
`P "Show basic information:";
+
`Pre " mlgpx info file.gpx";
+
`P "Show detailed information with waypoint details:";
+
`Pre " mlgpx info -v file.gpx";
+
] in
+
let input_arg =
+
let doc = "GPX file to analyze" in
+
Arg.(required & pos 0 (some non_dir_file) None & info [] ~docv:"FILE" ~doc) in
+
let term = Term.(const info_command $ input_arg $ verbose_flag
+
$ Fmt_cli.style_renderer ()) in
+
Cmd.v (Cmd.info "info" ~doc ~man) term
+
+
(* Main CLI *)
+
let main_cmd =
+
let doc = "mlgpx - GPX file manipulation toolkit" in
+
let man = [
+
`S Manpage.s_description;
+
`P "mlgpx is a command-line toolkit for working with GPX (GPS Exchange Format) \
+
files. It provides tools for converting, analyzing, and manipulating GPS data.";
+
`S Manpage.s_commands;
+
`P "Available commands:";
+
`P "$(b,convert) - Convert waypoints to trackset";
+
`P "$(b,info) - Display GPX file information";
+
`S Manpage.s_common_options;
+
`P "$(b,--verbose), $(b,-v) - Enable verbose output";
+
`P "$(b,--color)={auto|always|never} - Control ANSI color output";
+
`P "$(b,--help) - Show command help";
+
`S Manpage.s_examples;
+
`P "Convert waypoints to track:";
+
`Pre " mlgpx convert waypoints.gpx track.gpx";
+
`P "Analyze a GPX file with colors:";
+
`Pre " mlgpx info --verbose --color=always file.gpx";
+
`P "Convert without colors for scripts:";
+
`Pre " mlgpx convert --color=never waypoints.gpx track.gpx";
+
`S Manpage.s_bugs;
+
`P "Report bugs at https://github.com/avsm/mlgpx/issues";
+
] in
+
let default_term = Term.(ret (const (`Help (`Pager, None)))) in
+
Cmd.group (Cmd.info "mlgpx" ~version:"0.1.0" ~doc ~man) ~default:default_term
+
[convert_cmd; info_cmd]
+
+
let () =
+
Printexc.record_backtrace true;
+
exit (Cmd.eval main_cmd)
+5 -1
dune-project
···
(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)
+
(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 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)")
)
example_output.gpx

This is a binary file and will not be displayed.

-5
examples/dune
···
(public_name simple_gpx)
(name simple_gpx)
(libraries gpx xmlm))
-
-
(executable
-
(public_name effects_example)
-
(name effects_example)
-
(libraries gpx xmlm))
-117
examples/effects_example.ml
···
-
(** Simple GPX example demonstrating basic functionality **)
-
-
open Gpx
-
-
let () =
-
Printf.printf "=== Simple GPX Example ===\n\n";
-
-
try
-
(* Create some GPS coordinates *)
-
let lat1 = Coordinate.latitude 37.7749 |> Result.get_ok in
-
let lon1 = Coordinate.longitude (-122.4194) |> Result.get_ok in
-
let lat2 = Coordinate.latitude 37.7849 |> Result.get_ok in
-
let lon2 = Coordinate.longitude (-122.4094) |> Result.get_ok in
-
-
(* Create waypoints *)
-
let waypoint1 = Waypoint.make lat1 lon1 in
-
let waypoint1 = Waypoint.with_name waypoint1 "San Francisco" in
-
let waypoint2 = Waypoint.make lat2 lon2 in
-
let waypoint2 = Waypoint.with_name waypoint2 "Near SF" in
-
-
(* Create a simple track from coordinates *)
-
let track = Track.make ~name:"SF Walk" in
-
let track_segment = Track.Segment.empty in
-
let coords = [
-
(37.7749, -122.4194);
-
(37.7759, -122.4184);
-
(37.7769, -122.4174);
-
(37.7779, -122.4164);
-
] 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 coords in
-
let track = Track.add_segment track track_segment in
-
-
(* Create a route *)
-
let route = Route.make ~name:"SF Route" in
-
let route_coords = [
-
(37.7749, -122.4194);
-
(37.7849, -122.4094);
-
] in
-
let route = List.fold_left (fun r (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
-
Route.add_point r pt
-
| _ -> r
-
) route route_coords in
-
-
(* Create GPX document with all elements *)
-
let gpx = Gpx_doc.empty ~creator:"simple-example" in
-
let gpx = Gpx_doc.add_waypoint gpx waypoint1 in
-
let gpx = Gpx_doc.add_waypoint gpx waypoint2 in
-
let gpx = Gpx_doc.add_track gpx track in
-
let gpx = Gpx_doc.add_route gpx route in
-
-
Printf.printf "Created GPX document with:\n";
-
Printf.printf " Waypoints: %d\n" (List.length (Gpx_doc.get_waypoints gpx));
-
Printf.printf " Tracks: %d\n" (List.length (Gpx_doc.get_tracks gpx));
-
Printf.printf " Routes: %d\n" (List.length (Gpx_doc.get_routes gpx));
-
Printf.printf "\n";
-
-
(* Write to file with validation *)
-
let out_chan = open_out "example_output.gpx" in
-
let dest = (`Channel out_chan) in
-
(match write ~validate:true dest gpx with
-
| Ok () ->
-
close_out out_chan;
-
Printf.printf "Wrote GPX to example_output.gpx\n"
-
| Error e ->
-
close_out out_chan;
-
Printf.eprintf "Error writing GPX: %s\n" (Error.to_string e)
-
);
-
-
(* Read it back and verify *)
-
let in_chan = open_in "example_output.gpx" in
-
let input = Xmlm.make_input (`Channel in_chan) in
-
(match parse ~validate:true input with
-
| Ok gpx2 ->
-
close_in in_chan;
-
let waypoints = Gpx_doc.get_waypoints gpx2 in
-
let tracks = Gpx_doc.get_tracks gpx2 in
-
let routes = Gpx_doc.get_routes gpx2 in
-
Printf.printf "Read back GPX document with %d waypoints, %d tracks, %d routes\n"
-
(List.length waypoints) (List.length tracks) (List.length routes);
-
-
(* Extract coordinates from track *)
-
(match tracks with
-
| track :: _ ->
-
let segments = Track.get_segments track in
-
(match segments with
-
| seg :: _ ->
-
let points = Track.Segment.get_points seg in
-
Printf.printf "Track coordinates: %d points\n" (List.length points);
-
List.iteri (fun i pt ->
-
let lat = Coordinate.latitude_to_float (Waypoint.get_lat pt) in
-
let lon = Coordinate.longitude_to_float (Waypoint.get_lon pt) in
-
Printf.printf " Point %d: %.4f, %.4f\n" i lat lon
-
) points
-
| [] -> Printf.printf "No track segments found\n")
-
| [] -> Printf.printf "No tracks found\n")
-
| Error e ->
-
close_in in_chan;
-
Printf.eprintf "Error reading back GPX: %s\n" (Error.to_string e));
-
-
Printf.printf "\nExample completed successfully!\n"
-
-
with
-
| Gpx_error err ->
-
Printf.eprintf "GPX Error: %s\n" (Error.to_string err);
-
exit 1
-
| exn ->
-
Printf.eprintf "Unexpected error: %s\n" (Printexc.to_string exn);
-
exit 1
+9 -12
examples/simple_gpx.ml
···
match result with
| Ok (lat, lon) ->
let wpt = Waypoint.make lat lon in
-
let wpt = Waypoint.with_name wpt "San Francisco" in
-
let wpt = Waypoint.with_description wpt "Golden Gate Bridge area" in
-
Printf.printf "โœ“ Created waypoint: %s\n" (Option.value (Waypoint.get_name wpt) ~default:"<unnamed>");
+
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 = Gpx_doc.empty ~creator:"mlgpx direct API example" in
-
let gpx = Gpx_doc.add_waypoint gpx wpt in
+
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 metadata "Example GPX File" in
-
let metadata = Metadata.with_description metadata "Demonstration of mlgpx library capabilities" in
-
let gpx = Gpx_doc.with_metadata gpx metadata 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_comment track "Sample GPS track" in
-
let track = Track.with_description track "Demonstrates track creation" 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
···
) track_segment points in
let track = Track.add_segment track track_segment in
-
let gpx = Gpx_doc.add_track gpx track in
+
let gpx = Doc.add_track gpx track in
Printf.printf "โœ“ Created track\n";
···
Printf.printf "โœ“ Round-trip validation: %s\n"
(if validation2.is_valid then "PASSED" else "FAILED");
Printf.printf " Waypoints: %d, Tracks: %d\n"
-
(List.length (Gpx_doc.get_waypoints gpx2)) (List.length (Gpx_doc.get_tracks gpx2))
+
(List.length (Doc.waypoints gpx2)) (List.length (Doc.tracks gpx2))
| Error e ->
Printf.printf "โœ— Error reading back: %s\n" (Error.to_string e)
)
+2 -2
lib/gpx/coordinate.ml
···
| Error e, _ | _, Error e -> Error e
(** Extract components *)
-
let get_lat t = t.lat
-
let get_lon t = t.lon
+
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 *)
+8 -8
lib/gpx/coordinate.mli
···
(** Geographic coordinate types with validation *)
-
(** Private coordinate types with validation constraints *)
+
(** Coordinate types with validation constraints *)
type latitude = private float
type longitude = private float
type degrees = private float
-
(** Coordinate pair - main type for this module *)
+
(** Coordinate pair *)
type t = {
lat : latitude;
lon : longitude;
}
-
(** {2 Smart Constructors} *)
+
(** {2 Constructors} *)
(** Create validated latitude.
@param f Latitude in degrees (-90.0 to 90.0)
···
(** Convert degrees to float *)
val degrees_to_float : degrees -> float
-
(** {2 Coordinate Operations} *)
+
(** {2 Operations} *)
(** Create coordinate pair from validated components *)
val make : latitude -> longitude -> t
···
val make_from_floats : float -> float -> (t, string) result
(** Extract latitude component *)
-
val get_lat : t -> latitude
+
val lat : t -> latitude
(** Extract longitude component *)
-
val get_lon : t -> longitude
+
val lon : t -> longitude
(** Convert coordinate to float pair *)
val to_floats : t -> float * float
-
(** {2 Comparison and Utilities} *)
+
(** {2 Comparison and Printers} *)
(** Compare two coordinates *)
val compare : t -> t -> int
···
val equal : t -> t -> bool
(** Pretty print coordinate *)
-
val pp : Format.formatter -> t -> unit
+
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 parser writer validate coordinate link extension waypoint metadata route track error gpx_doc))
+
(modules
+
gpx
+
parser
+
writer
+
validate
+
coordinate
+
link
+
extension
+
waypoint
+
metadata
+
route
+
track
+
error
+
doc))
+8 -8
lib/gpx/extension.ml
···
(** {2 Extension Operations} *)
(** Create extension with flexible content *)
-
let make ~namespace ~name ~attributes ~content () =
+
let make ?namespace ~name ~attributes ~content () =
{ namespace; name; attributes; content }
(** Create an extension with text content *)
···
{ namespace; name; attributes; content = Mixed (text, elements) }
(** Get extension name *)
-
let get_name t = t.name
+
let name t = t.name
(** Get optional namespace *)
-
let get_namespace t = t.namespace
+
let namespace t = t.namespace
(** Get attributes *)
-
let get_attributes t = t.attributes
+
let attributes t = t.attributes
(** Get content *)
-
let get_content t = t.content
+
let content t = t.content
(** Create text content *)
let text_content text = Text text
···
let is_mixed_content = function Mixed _ -> true | _ -> false
(** Extract text content *)
-
let get_text_content = function Text s -> Some s | _ -> None
+
let text_content_extract = function Text s -> Some s | _ -> None
(** Extract element content *)
-
let get_elements_content = function Elements e -> Some e | _ -> None
+
let elements_content_extract = function Elements e -> Some e | _ -> None
(** Extract mixed content *)
-
let get_mixed_content = function Mixed (s, e) -> Some (s, e) | _ -> None
+
let mixed_content_extract = function Mixed (s, e) -> Some (s, e) | _ -> None
+8 -8
lib/gpx/extension.mli
···
(** {2 Extension Constructors} *)
(** Create extension with flexible content *)
-
val make : namespace:string option -> name:string -> attributes:(string * string) list -> content:content -> unit -> t
+
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
···
(** {2 Extension Operations} *)
(** Get extension name *)
-
val get_name : t -> string
+
val name : t -> string
(** Get optional namespace *)
-
val get_namespace : t -> string option
+
val namespace : t -> string option
(** Get attributes *)
-
val get_attributes : t -> (string * string) list
+
val attributes : t -> (string * string) list
(** Get content *)
-
val get_content : t -> content
+
val content : t -> content
(** Find attribute value by name *)
val find_attribute : string -> t -> string option
···
val is_mixed_content : content -> bool
(** Extract text content *)
-
val get_text_content : content -> string option
+
val text_content_extract : content -> string option
(** Extract element content *)
-
val get_elements_content : content -> t list option
+
val elements_content_extract : content -> t list option
(** Extract mixed content *)
-
val get_mixed_content : content -> (string * t list) option
+
val mixed_content_extract : content -> (string * t list) option
+6 -6
lib/gpx/gpx.ml
···
module Error = Error
(** Main GPX document type *)
-
module Gpx_doc = Gpx_doc
+
module Doc = Doc
(** {1 Main Document Type} *)
(** Main GPX document type *)
-
type t = Gpx_doc.t
+
type t = Doc.t
(** {1 Error Handling} *)
···
let is_valid = Validate.is_valid
(** Get only error messages *)
-
let get_errors = Validate.get_errors
+
let errors = Validate.errors
(** Get only warning messages *)
-
let get_warnings = Validate.get_warnings
+
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 = Gpx_doc.empty ~creator
+
let make_gpx ~creator = Doc.empty ~creator
(** Create empty GPX document *)
-
let empty ~creator = Gpx_doc.empty ~creator
+
let empty ~creator = Doc.empty ~creator
+443 -54
lib/gpx/gpx.mli
···
-
(** OCaml library for reading and writing GPX (GPS Exchange Format) files
+
(** OCaml library for reading and writing GPX (GPS Exchange Format) files.
+
+
{1 Overview}
+
+
GPX (GPS Exchange Format) is an XML-based format for GPS data interchange,
+
standardized by {{:https://www.topografix.com/gpx.asp}Topografix}. This library
+
provides a complete implementation of the GPX 1.1 specification with strong
+
type safety and validation.
+
+
GPX files can contain three main types of GPS data:
+
- {b Waypoints}: Individual points of interest with coordinates
+
- {b Routes}: Ordered sequences of waypoints representing a planned path
+
- {b Tracks}: Recorded GPS traces, typically from actual journeys
+
+
All coordinates in GPX use the WGS84 datum (World Geodetic System 1984),
+
the same coordinate system used by GPS satellites. Coordinates are expressed
+
as decimal degrees, with elevations in meters above mean sea level.
+
+
{2 Quick Start Example}
+
+
{[
+
open Gpx
+
+
(* Create coordinates *)
+
let lat = Coordinate.latitude 37.7749 |> Result.get_ok in
+
let lon = Coordinate.longitude (-122.4194) |> Result.get_ok in
+
+
(* Create a waypoint *)
+
let waypoint = Waypoint.make lat lon
+
|> Waypoint.with_name "San Francisco"
+
|> Waypoint.with_description "Golden Gate Bridge area" in
+
+
(* Create GPX document *)
+
let gpx = make_gpx ~creator:"my-app"
+
|> Doc.add_waypoint waypoint in
+
+
(* Write to file or string *)
+
match write_string gpx with
+
| Ok xml -> print_endline xml
+
| Error e -> Printf.eprintf "Error: %s\n" (Error.to_string e)
+
]}
This library provides a clean, modular interface for working with GPX files,
-
the standard format for GPS data exchange. *)
+
with separate modules for each major component of the GPX specification. *)
(** {1 Core Modules}
The library is organized into focused modules, each handling a specific aspect
-
of GPX data. *)
+
of GPX data. Each module provides complete functionality for its domain with
+
strong type safety and validation. *)
-
(** Geographic coordinate handling with validation *)
+
(** {2 Geographic coordinate handling with validation}
+
+
The {!Coordinate} module provides validated coordinate types for latitude,
+
longitude, and degrees. All coordinates use the WGS84 datum and are validated
+
at construction time to ensure they fall within valid ranges:
+
- Latitude: -90.0 to +90.0 degrees
+
- Longitude: -180.0 to +180.0 degrees
+
- Degrees: 0.0 to 360.0 degrees
+
+
Example: [Coordinate.latitude 37.7749] creates a validated latitude. *)
module Coordinate = Coordinate
-
(** Links, persons, and copyright information *)
+
(** {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
-
(** Extension mechanism for custom GPX elements *)
+
(** {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
-
(** GPS waypoint data and fix types *)
+
(** {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
-
(** GPX metadata including bounds *)
+
(** {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 data and calculations *)
+
(** {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
-
(** Track data with segments *)
+
(** {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
-
(** Error handling *)
+
(** {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
-
(** Main GPX document type *)
-
module Gpx_doc = Gpx_doc
+
(** {2 Main GPX document type}
+
+
The {!Doc} module represents complete GPX documents containing:
+
- Document metadata (creator, version)
+
- Collections of waypoints, routes, and tracks
+
- Document-level extensions
+
- Statistics and analysis functions *)
+
module Doc = Doc
(** {1 Main Document Type} *)
-
(** Main GPX document type *)
-
type t = Gpx_doc.t
+
(** A complete GPX document containing waypoints, routes, tracks, and metadata.
+
+
This is the main type representing a complete GPX file. GPX documents must
+
have a creator string (identifying the creating application) and follow the
+
GPX 1.1 specification format. *)
+
type t = Doc.t
(** {1 Error Handling} *)
-
(** Error types *)
+
(** Comprehensive error type covering all possible GPX operation failures.
+
+
Errors can occur during:
+
- XML parsing (malformed XML, invalid structure)
+
- Coordinate validation (out of range values)
+
- Missing required GPX elements or attributes
+
- File I/O operations *)
type error = Error.t
-
(** GPX exception raised for 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
-
(** {1 Parsing Functions} *)
+
(** {1 Parsing Functions}
+
+
Parse GPX data from various sources. All parsing functions support optional
+
validation to check compliance with GPX specification constraints. *)
-
(** Parse GPX from XML input.
+
(** Parse GPX from XML input source.
-
@param validate Whether to validate the document after parsing
-
@param input XMLm input source
-
@return Parsed GPX document or error *)
+
Reads GPX data from an {!Xmlm.input} source, which can be created from
+
files, strings, or other input sources using the {{:https://erratique.ch/software/xmlm}Xmlm} library.
+
+
@param validate If [true] (default [false]), validates the parsed document
+
against GPX specification rules. Validation checks coordinate
+
ranges, required elements, and data consistency.
+
@param input XMLm input source created with [Xmlm.make_input]
+
@return [Ok gpx] with parsed document, or [Error e] if parsing fails
+
+
Example:
+
{[
+
let input = Xmlm.make_input (`String (0, gpx_xml_string)) in
+
match parse ~validate:true input with
+
| Ok gpx -> Printf.printf "Parsed %d waypoints\n" (List.length (Doc.waypoints gpx))
+
| Error e -> Printf.eprintf "Parse error: %s\n" (Error.to_string e)
+
]} *)
val parse : ?validate:bool -> Xmlm.input -> (t, error) result
-
(** Parse GPX from string.
+
(** 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 Whether to validate the document after parsing
-
@param s XML string to parse
-
@return Parsed GPX document or error *)
+
@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
-
(** {1 Writing Functions} *)
+
(** {1 Writing Functions}
+
+
Generate GPX XML from document structures. All writing functions support
+
optional validation before output generation. *)
-
(** Write GPX to XML output.
+
(** Write GPX to XML output destination.
-
@param validate Whether to validate before writing
-
@param output XMLm 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 Success or error *)
+
@return [Ok ()] on success, or [Error e] if writing fails
+
+
Example:
+
{[
+
let output = Buffer.create 1024 in
+
let dest = Xmlm.make_output (`Buffer output) in
+
match write ~validate:true dest gpx with
+
| Ok () -> Buffer.contents output
+
| Error e -> failwith (Error.to_string e)
+
]} *)
val write : ?validate:bool -> Xmlm.dest -> t -> (unit, error) result
-
(** Write GPX to string.
+
(** 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
-
@param validate Whether to validate before writing
-
@param gpx GPX document to write
-
@return XML string or error *)
+
Example:
+
{[
+
match write_string ~validate:true gpx with
+
| Ok xml ->
+
print_endline "Generated GPX:";
+
print_endline xml
+
| Error e ->
+
Printf.eprintf "Failed to generate GPX: %s\n" (Error.to_string e)
+
]} *)
val write_string : ?validate:bool -> t -> (string, error) result
(** {1 Validation Functions}
-
Validate GPX documents for correctness and best practices. *)
+
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. *)
-
(** 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; (** Whether document is valid *)
+
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 *)
+
(** 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 - returns true if document is valid *)
+
(** 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 messages *)
-
val get_errors : t -> 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 messages *)
-
val get_warnings : t -> validation_issue list
+
(** Get only validation warnings (best practice violations).
+
+
Returns only the issues marked as warnings. These don't make the document
+
invalid but may indicate potential problems or areas for improvement.
+
+
@param gpx The GPX document to validate
+
@return List of warning-level validation issues *)
+
val warnings : t -> validation_issue list
-
(** Format validation issue for display *)
+
(** Format a validation issue for human-readable display.
+
+
Combines the issue message with location context if available.
+
+
@param issue The validation issue to format
+
@return Formatted string suitable for display to users
+
+
Example output: ["Error in waypoint 1: Latitude out of range (-95.0)"] *)
val format_issue : validation_issue -> string
-
(** {1 Constructors and Utilities} *)
+
(** {1 Document Constructors and Utilities}
+
+
Functions for creating GPX documents and basic document operations. *)
-
(** Create new GPX document with required fields *)
+
(** Create a new GPX document with the required creator field.
+
+
Every GPX document must identify its creating application through the
+
[creator] attribute. This is required by the GPX specification and helps
+
identify the source of GPS data.
+
+
The created document:
+
- Uses GPX version 1.1 (the current standard)
+
- Contains no waypoints, routes, or tracks initially
+
- Has no metadata initially
+
- Can be extended using {!Doc} module functions
+
+
@param creator Name of the creating application (e.g., "MyGPS App v1.0")
+
@return Empty GPX document ready for data addition
+
+
Example:
+
{[
+
let gpx = make_gpx ~creator:"MyTracker v2.1" in
+
let gpx = Doc.add_waypoint gpx some_waypoint in
+
let gpx = Doc.add_track gpx some_track in
+
(* gpx now contains waypoints and tracks *)
+
]} *)
val make_gpx : creator:string -> t
-
(** Create empty GPX document *)
-
val empty : creator:string -> t
+
(** Create an empty GPX document with the required creator field.
+
+
Alias for {!make_gpx} provided for consistency with module naming patterns.
+
Creates a document with no GPS data that can be populated using the
+
{!Doc} module functions.
+
+
@param creator Name of the creating application
+
@return Empty GPX document
+
+
Example:
+
{[
+
let gpx = empty ~creator:"GPS Logger" in
+
assert (List.length (Doc.waypoints gpx) = 0);
+
assert (List.length (Doc.tracks gpx) = 0);
+
assert (Doc.creator gpx = "GPS Logger");
+
]} *)
+
val empty : creator:string -> t
+
+
(** {1 Common Patterns and Best Practices}
+
+
{2 Reading GPX Files}
+
+
The most common use case is reading existing GPX files:
+
{[
+
(* From a file using platform-specific modules *)
+
match Gpx_unix.read "track.gpx" with
+
| Ok gpx -> process_gpx gpx
+
| Error e -> handle_error e
+
+
(* From a string *)
+
match parse_string ~validate:true gpx_content with
+
| Ok gpx -> process_gpx gpx
+
| Error e -> handle_error e
+
]}
+
+
{2 Creating GPX Files}
+
+
To create new GPX files with waypoints:
+
{[
+
(* Create coordinates *)
+
let lat = Coordinate.latitude 37.7749 |> Result.get_ok in
+
let lon = Coordinate.longitude (-122.4194) |> Result.get_ok in
+
+
(* Create waypoint *)
+
let waypoint = Waypoint.make lat lon
+
|> Waypoint.with_name "Golden Gate"
+
|> Waypoint.with_description "Famous San Francisco bridge" in
+
+
(* Create document *)
+
let gpx = make_gpx ~creator:"My App v1.0"
+
|> Doc.add_waypoint waypoint in
+
+
(* Write to file *)
+
match Gpx_unix.write "output.gpx" gpx with
+
| Ok () -> print_endline "File written successfully"
+
| Error e -> Printf.eprintf "Write error: %s\n" (Error.to_string e)
+
]}
+
+
{2 Working with Tracks}
+
+
Tracks represent recorded GPS traces with timestamped points:
+
{[
+
(* Create track points with timestamps *)
+
let points = List.map (fun (lat_f, lon_f, time) ->
+
let lat = Coordinate.latitude lat_f |> Result.get_ok in
+
let lon = Coordinate.longitude lon_f |> Result.get_ok in
+
Waypoint.make lat lon |> Waypoint.with_time (Some time)
+
) gps_data in
+
+
(* Create track segment *)
+
let segment = Track.Segment.make points in
+
+
(* Create track *)
+
let track = Track.make ~name:"Morning Run"
+
|> Track.add_segment segment in
+
+
(* Add to document *)
+
let gpx = make_gpx ~creator:"Fitness App"
+
|> Doc.add_track track
+
]}
+
+
{2 Coordinate Systems and Units}
+
+
- All coordinates use WGS84 datum (World Geodetic System 1984)
+
- Latitude ranges from -90.0 (South Pole) to +90.0 (North Pole)
+
- Longitude ranges from -180.0 to +180.0 degrees
+
- Elevations are in meters above mean sea level
+
- Times use RFC 3339 format (ISO 8601 subset)
+
+
{2 Validation Recommendations}
+
+
- Always validate when parsing untrusted GPX data
+
- Validate before writing to catch data consistency issues
+
- Handle both errors and warnings appropriately
+
- Use {!val:is_valid} for quick checks, {!validate_gpx} for detailed analysis
+
+
{2 Performance Considerations}
+
+
- The library uses streaming XML parsing for memory efficiency
+
- Large GPX files with many track points are handled efficiently
+
- Coordinate validation occurs at construction time
+
- Consider using platform-specific modules ({!Gpx_unix}, [Gpx_eio]) for file I/O
+
+
{2 Extension Support}
+
+
The library supports GPX extensions for custom data:
+
{[
+
(* Create extension *)
+
let ext = Extension.make_text
+
~name:"temperature"
+
~namespace:"http://example.com/weather"
+
"25.5" in
+
+
(* Add to waypoint *)
+
let waypoint = Waypoint.make lat lon
+
|> Waypoint.add_extensions [ext]
+
]} *)
+
+
(** {1 Related Modules and Libraries}
+
+
This core module provides the foundation. For complete applications, consider:
+
+
- {!Gpx_unix}: File I/O operations using standard Unix libraries
+
- {!Gpx_eio}: Concurrent file I/O using the Eio effects library
+
- {{:https://erratique.ch/software/xmlm}Xmlm}: Underlying XML processing library
+
- {{:https://erratique.ch/software/ptime}Ptime}: Time representation used for timestamps
+
+
{2 External Links}
+
+
- {{:https://www.topografix.com/gpx.asp}Official GPX specification}
+
- {{:https://www.topografix.com/GPX/1/1/gpx.xsd}GPX 1.1 XML Schema}
+
- {{:https://en.wikipedia.org/wiki/GPS_Exchange_Format}GPX Format on Wikipedia}
+
- {{:https://en.wikipedia.org/wiki/World_Geodetic_System}WGS84 Coordinate System} *)
-196
lib/gpx/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 get_version t = t.version
-
-
(** Get creator *)
-
let get_creator t = t.creator
-
-
(** Get metadata *)
-
let get_metadata t = t.metadata
-
-
(** Get waypoints *)
-
let get_waypoints t = t.waypoints
-
-
(** Get routes *)
-
let get_routes t = t.routes
-
-
(** Get tracks *)
-
let get_tracks t = t.tracks
-
-
(** Get extensions *)
-
let get_extensions t = t.extensions
-
-
(** {2 Document Modification} *)
-
-
(** Set version *)
-
let with_version t version = { t with version }
-
-
(** Set metadata *)
-
let with_metadata t metadata = { t with metadata = Some metadata }
-
-
(** Set metadata *)
-
let set_metadata metadata t = { 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.get_elevation wpt <> None) t.waypoints ||
-
List.exists (fun route ->
-
List.exists (fun pt -> Waypoint.get_elevation pt <> None) (Route.get_points route)
-
) t.routes ||
-
List.exists (fun track ->
-
List.exists (fun pt -> Waypoint.get_elevation pt <> None) (Track.all_points track)
-
) t.tracks
-
-
(** Check if document has time data *)
-
let has_time t =
-
List.exists (fun wpt -> Waypoint.get_time wpt <> None) t.waypoints ||
-
List.exists (fun route ->
-
List.exists (fun pt -> Waypoint.get_time pt <> None) (Route.get_points route)
-
) t.routes ||
-
List.exists (fun track ->
-
List.exists (fun pt -> Waypoint.get_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 get_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;
-
}
-
-
(** {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 = get_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
-
-
(** Print document statistics *)
-
let print_stats t =
-
let stats = get_stats t in
-
Printf.printf "GPX Statistics:\n";
-
Printf.printf " Version: %s\n" t.version;
-
Printf.printf " Creator: %s\n" t.creator;
-
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")
-134
lib/gpx/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 get_version : t -> string
-
-
(** Get creator *)
-
val get_creator : t -> string
-
-
(** Get metadata *)
-
val get_metadata : t -> Metadata.t option
-
-
(** Get waypoints *)
-
val get_waypoints : t -> Waypoint.t list
-
-
(** Get routes *)
-
val get_routes : t -> Route.t list
-
-
(** Get tracks *)
-
val get_tracks : t -> Track.t list
-
-
(** Get extensions *)
-
val get_extensions : t -> Extension.t list
-
-
(** {2 Document Modification} *)
-
-
(** Set version *)
-
val with_version : t -> string -> t
-
-
(** Set metadata *)
-
val with_metadata : t -> Metadata.t -> t
-
-
(** Set metadata *)
-
val set_metadata : Metadata.t -> 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 get_stats : t -> stats
-
-
(** {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
-
-
(** Print document statistics to stdout *)
-
val print_stats : t -> unit
+11 -11
lib/gpx/link.ml
···
let make ~href ?text ?type_ () = { href; text; type_ }
(** Get href from link *)
-
let get_href t = t.href
+
let href t = t.href
(** Get optional text from link *)
-
let get_text t = t.text
+
let text t = t.text
(** Get optional type from link *)
-
let get_type t = t.type_
+
let type_ t = t.type_
-
(** Set text on link *)
+
(** Update text *)
let with_text t text = { t with text = Some text }
-
(** Set type on link *)
+
(** Update type *)
let with_type t type_ = { t with type_ = Some type_ }
(** Compare links *)
···
let make_person ?name ?email ?link () = { name; email; link }
(** Get person name *)
-
let get_person_name (p : person) = p.name
+
let person_name (p : person) = p.name
(** Get person email *)
-
let get_person_email (p : person) = p.email
+
let person_email (p : person) = p.email
(** Get person link *)
-
let get_person_link (p : person) = p.link
+
let person_link (p : person) = p.link
(** Compare persons *)
let compare_person p1 p2 =
···
let make_copyright ~author ?year ?license () = { author; year; license }
(** Get copyright author *)
-
let get_copyright_author (c : copyright) = c.author
+
let copyright_author (c : copyright) = c.author
(** Get copyright year *)
-
let get_copyright_year (c : copyright) = c.year
+
let copyright_year (c : copyright) = c.year
(** Get copyright license *)
-
let get_copyright_license (c : copyright) = c.license
+
let copyright_license (c : copyright) = c.license
(** Compare copyrights *)
let compare_copyright c1 c2 =
+12 -11
lib/gpx/link.mli
···
val make : href:string -> ?text:string -> ?type_:string -> unit -> t
(** Get href from link *)
-
val get_href : t -> string
+
val href : t -> string
(** Get optional text from link *)
-
val get_text : t -> string option
+
val text : t -> string option
(** Get optional type from link *)
-
val get_type : t -> string option
+
val type_ : t -> string option
-
(** Set text on link *)
+
(** Update text *)
val with_text : t -> string -> t
-
(** Set type on link *)
+
(** Update type *)
val with_type : t -> string -> t
+
(** Compare links *)
val compare : t -> t -> int
···
val make_person : ?name:string -> ?email:string -> ?link:t -> unit -> person
(** Get person name *)
-
val get_person_name : person -> string option
+
val person_name : person -> string option
(** Get person email *)
-
val get_person_email : person -> string option
+
val person_email : person -> string option
(** Get person link *)
-
val get_person_link : person -> t option
+
val person_link : person -> t option
(** Compare persons *)
val compare_person : person -> person -> int
···
val make_copyright : author:string -> ?year:int -> ?license:string -> unit -> copyright
(** Get copyright author *)
-
val get_copyright_author : copyright -> string
+
val copyright_author : copyright -> string
(** Get copyright year *)
-
val get_copyright_year : copyright -> int option
+
val copyright_year : copyright -> int option
(** Get copyright license *)
-
val get_copyright_license : copyright -> string option
+
val copyright_license : copyright -> string option
(** Compare copyrights *)
val compare_copyright : copyright -> copyright -> int
+31 -30
lib/gpx/metadata.ml
···
| Error e, _, _, _ | _, Error e, _, _ | _, _, Error e, _ | _, _, _, Error e -> Error e
(** Get corner coordinates *)
-
let get_min_coords t = Coordinate.make t.minlat t.minlon
-
let get_max_coords t = Coordinate.make t.maxlat t.maxlon
+
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 get_bounds t = (t.minlat, t.minlon, t.maxlat, t.maxlon)
+
let bounds t = (t.minlat, t.minlon, t.maxlat, t.maxlon)
(** Check if coordinate is within bounds *)
let contains bounds coord =
-
let lat = Coordinate.get_lat coord in
-
let lon = Coordinate.get_lon coord in
+
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 &&
···
let make ~name = { empty with name = Some name }
(** Get name *)
-
let get_name t = t.name
+
let name t = t.name
(** Get description *)
-
let get_description t = t.desc
+
let description t = t.desc
(** Get author *)
-
let get_author t = t.author
+
let author t = t.author
(** Get copyright *)
-
let get_copyright t = t.copyright
+
let copyright t = t.copyright
(** Get links *)
-
let get_links t = t.links
+
let links t = t.links
(** Get time *)
-
let get_time t = t.time
+
let time t = t.time
(** Get keywords *)
-
let get_keywords t = t.keywords
+
let keywords t = t.keywords
(** Get bounds *)
-
let get_bounds t = t.bounds
+
let bounds_opt t = t.bounds
-
(** Set name *)
-
let set_name name t = { t with name = Some name }
+
(** Get extensions *)
+
let extensions t = t.extensions
-
(** Set description *)
-
let set_description desc t = { t with desc = Some desc }
-
-
(** Set author *)
-
let set_author author t = { t with author = Some author }
-
-
(** Add link *)
-
let add_link t link = { t with links = link :: t.links }
-
-
(** Functional setters for building metadata *)
-
-
(** Set name *)
+
(** Update name *)
let with_name t name = { t with name = Some name }
-
(** Set description *)
+
(** Update description *)
let with_description t desc = { t with desc = Some desc }
-
(** Set keywords *)
+
(** Update keywords *)
let with_keywords t keywords = { t with keywords = Some keywords }
-
(** Set time *)
+
(** 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 }
+27 -24
lib/gpx/metadata.mli
···
val make_from_floats : minlat:float -> minlon:float -> maxlat:float -> maxlon:float -> (t, string) result
(** Get minimum corner coordinates *)
-
val get_min_coords : t -> Coordinate.t
+
val min_coords : t -> Coordinate.t
(** Get maximum corner coordinates *)
-
val get_max_coords : t -> Coordinate.t
+
val max_coords : t -> Coordinate.t
(** Get all bounds as tuple *)
-
val get_bounds : t -> (Coordinate.latitude * Coordinate.longitude * Coordinate.latitude * Coordinate.longitude)
+
val bounds : t -> (Coordinate.latitude * Coordinate.longitude * Coordinate.latitude * Coordinate.longitude)
(** Check if coordinate is within bounds *)
val contains : t -> Coordinate.t -> bool
···
val make : name:string -> t
(** Get name *)
-
val get_name : t -> string option
+
val name : t -> string option
(** Get description *)
-
val get_description : t -> string option
+
val description : t -> string option
(** Get author *)
-
val get_author : t -> Link.person option
+
val author : t -> Link.person option
(** Get copyright *)
-
val get_copyright : t -> Link.copyright option
+
val copyright : t -> Link.copyright option
(** Get links *)
-
val get_links : t -> Link.t list
+
val links : t -> Link.t list
(** Get time *)
-
val get_time : t -> Ptime.t option
+
val time : t -> Ptime.t option
(** Get keywords *)
-
val get_keywords : t -> string option
+
val keywords : t -> string option
(** Get bounds *)
-
val get_bounds : t -> bounds option
+
val bounds_opt : t -> bounds option
-
(** Set name *)
-
val set_name : string -> t -> t
+
(** Get extensions *)
+
val extensions : t -> Extension.t list
-
(** Set description *)
-
val set_description : string -> t -> t
+
(** Functional operations for building metadata *)
-
(** Set author *)
-
val set_author : Link.person -> t -> t
-
-
(** Functional setters for building metadata *)
-
-
(** Set name *)
+
(** Update name *)
val with_name : t -> string -> t
-
(** Set description *)
+
(** Update description *)
val with_description : t -> string -> t
-
(** Set keywords *)
+
(** Update keywords *)
val with_keywords : t -> string -> t
-
(** Set time *)
+
(** 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
+127 -9
lib/gpx/parser.ml
···
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 (Extension.make ~namespace ~name ~attributes ~content ())
+
Ok (Extension.make ?namespace ~name ~attributes ~content ())
and parse_extension_content parser =
Buffer.clear parser.text_buffer;
···
in
let* (version, creator) = find_gpx_root () in
-
let gpx = Gpx_doc.empty ~creator in
-
parse_gpx_elements parser (Gpx_doc.with_version gpx version)
+
let gpx = Doc.empty ~creator in
+
parse_gpx_elements parser { gpx with version }
and parse_gpx_elements parser gpx =
let rec loop gpx =
···
(match name with
| "metadata" ->
let* metadata = parse_metadata parser in
-
loop (Gpx_doc.with_metadata gpx 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_doc.add_waypoint gpx waypoint)
+
loop (Doc.add_waypoint gpx waypoint)
| "rte" ->
let* route = parse_route parser in
-
loop (Gpx_doc.add_route gpx route)
+
loop (Doc.add_route gpx route)
| "trk" ->
let* track = parse_track parser in
-
loop (Gpx_doc.add_track gpx track)
+
loop (Doc.add_track gpx track)
| "extensions" ->
let* extensions = parse_extensions parser in
-
loop (Gpx_doc.add_extensions gpx extensions)
+
loop (Doc.add_extensions gpx extensions)
| _ ->
let* _ = skip_element parser in
loop gpx)
···
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 = Metadata.empty in
let rec loop metadata =
···
| "link" ->
let* link = parse_link parser attrs in
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.add_extensions metadata extensions)
···
(** Parse from string *)
let parse_string ?(validate=false) s =
let input = Xmlm.make_input (`String (0, s)) in
-
parse ~validate input
+
parse ~validate input
+2 -2
lib/gpx/parser.mli
···
(** GPX streaming parser using xmlm *)
(** Parse a GPX document from an xmlm input source *)
-
val parse : ?validate:bool -> Xmlm.input -> (Gpx_doc.t, Error.t) result
+
val parse : ?validate:bool -> Xmlm.input -> (Doc.t, Error.t) result
(** Parse a GPX document from a string *)
-
val parse_string : ?validate:bool -> string -> (Gpx_doc.t, Error.t) result
+
val parse_string : ?validate:bool -> string -> (Doc.t, Error.t) result
+30 -17
lib/gpx/route.ml
···
let make_rtept (lat_f, lon_f) =
match Waypoint.make_from_floats ~lat:lat_f ~lon:lon_f () with
| Ok wpt -> wpt
-
| Error e -> failwith e
+
| Error e -> invalid_arg e
in
let rtepts = List.map make_rtept coords in
{ empty with name = Some name; rtepts }
(** Get route name *)
-
let get_name t = t.name
+
let name t = t.name
(** Get route description *)
-
let get_description t = t.desc
+
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 get_points t = t.rtepts
+
let points t = t.rtepts
(** Get route point count *)
let point_count t = List.length t.rtepts
-
(** Set name *)
-
let set_name name t = { t with name = Some name }
-
-
(** Set description *)
-
let set_description desc t = { t with desc = Some desc }
(** Clear all points *)
let clear_points t = { t with rtepts = [] }
···
| [] -> None
| p :: _ -> Some p
-
(** {2 Functional Setters} *)
+
(** {2 Functional Operations} *)
-
(** Set name *)
+
(** Update name *)
let with_name t name = { t with name = Some name }
-
(** Set comment *)
+
(** Update comment *)
let with_comment t cmt = { t with cmt = Some cmt }
-
(** Set description *)
+
(** Update description *)
let with_description t desc = { t with desc = Some desc }
-
(** Set source *)
+
(** Update source *)
let with_source t src = { t with src = Some src }
-
(** Set number *)
+
(** Update number *)
let with_number t number = { t with number = Some number }
-
(** Set type *)
+
(** Update type *)
let with_type t type_ = { t with type_ = Some type_ }
(** Add point *)
···
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)
+
| None -> Format.fprintf ppf "(unnamed route, %d points)" (point_count t)
+31 -19
lib/gpx/route.mli
···
(** Create route from coordinate list.
@param name Route name
@param coords List of (latitude, longitude) pairs
-
@raises Failure on invalid coordinates *)
+
@raises Invalid_argument on invalid coordinates *)
val make_from_coords : name:string -> (float * float) list -> t
(** {2 Route Properties} *)
(** Get route name *)
-
val get_name : t -> string option
+
val name : t -> string option
(** Get route description *)
-
val get_description : t -> string option
+
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 get_points : t -> point list
+
val points : t -> point list
(** Get route point count *)
val point_count : t -> int
···
val is_empty : t -> bool
(** {2 Route Modification} *)
-
-
(** Set name *)
-
val set_name : string -> t -> t
-
-
(** Set description *)
-
val set_description : string -> t -> t
(** Clear all points *)
val clear_points : t -> t
···
(** Get last point *)
val last_point : t -> point option
-
(** {2 Functional Setters} *)
+
(** {2 Functional Operations} *)
-
(** Set name *)
+
(** Update name *)
val with_name : t -> string -> t
-
(** Set comment *)
-
val with_comment : t -> string -> t
+
(** Update comment *)
+
val with_comment : t -> string -> t
-
(** Set description *)
+
(** Update description *)
val with_description : t -> string -> t
-
(** Set source *)
+
(** Update source *)
val with_source : t -> string -> t
-
(** Set number *)
+
(** Update number *)
val with_number : t -> int -> t
-
(** Set type *)
+
(** Update type *)
val with_type : t -> string -> t
(** Add point *)
···
val equal : t -> t -> bool
(** Pretty print route *)
-
val pp : Format.formatter -> t -> unit
+
val pp : Format.formatter -> t -> unit
+34 -18
lib/gpx/track.ml
···
let make_trkpt (lat_f, lon_f) =
match Waypoint.make_from_floats ~lat:lat_f ~lon:lon_f () with
| Ok wpt -> wpt
-
| Error e -> failwith e
+
| Error e -> invalid_arg e
in
let trkpts = List.map make_trkpt coords in
{ trkpts; extensions = [] }
(** Get points *)
-
let get_points t = t.trkpts
+
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] }
···
{ empty with name = Some name; trksegs = [segment] }
(** Get track name *)
-
let get_name t = t.name
+
let name t = t.name
(** Get track description *)
-
let get_description t = t.desc
+
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 get_segments t = t.trksegs
+
let segments t = t.trksegs
(** Get segment count *)
let segment_count t = List.length t.trksegs
···
let point_count t =
List.fold_left (fun acc seg -> acc + Segment.point_count seg) 0 t.trksegs
-
(** Set name *)
-
let set_name name t = { t with name = Some name }
-
-
(** Set description *)
-
let set_description desc t = { t with desc = Some desc }
(** Clear all segments *)
let clear_segments t = { t with trksegs = [] }
···
(** Test track equality *)
let equal t1 t2 = compare t1 t2 = 0
-
(** {2 Functional Setters} *)
+
(** {2 Functional Operations} *)
-
(** Set name *)
+
(** Update name *)
let with_name t name = { t with name = Some name }
-
(** Set comment *)
+
(** Update comment *)
let with_comment t cmt = { t with cmt = Some cmt }
-
(** Set description *)
+
(** Update description *)
let with_description t desc = { t with desc = Some desc }
-
(** Set source *)
+
(** Update source *)
let with_source t src = { t with src = Some src }
-
(** Set number *)
+
(** Update number *)
let with_number t number = { t with number = Some number }
-
(** Set type *)
+
(** Update type *)
let with_type t type_ = { t with type_ = Some type_ }
(** Add segment *)
···
| 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)
+
(segment_count t) (point_count t)
+34 -19
lib/gpx/track.mli
···
val make : point list -> t
(** Create segment from coordinate list.
-
@raises Failure on invalid coordinates *)
+
@raises Invalid_argument on invalid coordinates *)
val make_from_coords : (float * float) list -> t
(** Get points *)
-
val get_points : t -> point list
+
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
···
(** {2 Track Properties} *)
(** Get track name *)
-
val get_name : t -> string option
+
val name : t -> string option
(** Get track description *)
-
val get_description : t -> string option
+
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 get_segments : t -> segment list
+
val segments : t -> segment list
(** Get segment count *)
val segment_count : t -> int
···
val is_empty : t -> bool
(** {2 Track Modification} *)
-
-
(** Set name *)
-
val set_name : string -> t -> t
-
-
(** Set description *)
-
val set_description : string -> t -> t
(** Clear all segments *)
val clear_segments : t -> t
···
(** Test track equality *)
val equal : t -> t -> bool
-
(** {2 Functional Setters} *)
+
(** {2 Functional Operations} *)
-
(** Set name *)
+
(** Update name *)
val with_name : t -> string -> t
-
(** Set comment *)
+
(** Update comment *)
val with_comment : t -> string -> t
-
(** Set description *)
+
(** Update description *)
val with_description : t -> string -> t
-
(** Set source *)
+
(** Update source *)
val with_source : t -> string -> t
-
(** Set number *)
+
(** Update number *)
val with_number : t -> int -> t
-
(** Set type *)
+
(** Update type *)
val with_type : t -> string -> t
(** Add segment *)
···
val add_extensions : t -> Extension.t list -> t
(** Pretty print track *)
-
val pp : Format.formatter -> t -> unit
+
val pp : Format.formatter -> t -> unit
+23 -23
lib/gpx/validate.ml
···
let issues = ref [] in
(* Check for negative satellite count *)
-
(match Waypoint.get_sat wpt 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" (Waypoint.get_hdop wpt);
-
check_precision "vdop" (Waypoint.get_vdop wpt);
-
check_precision "pdop" (Waypoint.get_pdop wpt);
+
check_precision "hdop" (Waypoint.hdop wpt);
+
check_precision "vdop" (Waypoint.vdop wpt);
+
check_precision "pdop" (Waypoint.pdop wpt);
(* Check elevation reasonableness *)
-
(match Waypoint.get_elevation wpt 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 Waypoint.get_ageofdgpsdata wpt 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
-
let (minlat, minlon, maxlat, maxlon) = Metadata.Bounds.get_bounds bounds in
+
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;
···
let issues = ref [] in
(* Validate bounds if present *)
-
(match Metadata.get_bounds metadata with
+
(match Metadata.bounds_opt metadata with
| Some bounds -> issues := validate_bounds bounds @ !issues
| None -> ());
(* Check for reasonable copyright year *)
-
(match Metadata.get_copyright metadata with
+
(match Metadata.copyright metadata with
| Some copyright ->
-
(match Link.get_copyright_year copyright 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 *)
-
let points = Route.get_points route in
+
let points = Route.points route in
if points = [] then
issues := make_warning ~location "Route has no points" :: !issues;
···
let location = Printf.sprintf "track.trkseg[%d]" seg_idx in
(* Check for empty segment *)
-
let points = Track.Segment.get_points trkseg in
+
let points = Track.Segment.points trkseg in
if points = [] then
issues := make_warning ~location "Track segment has no points" :: !issues;
···
let rec check_time_order prev_time = function
| [] -> ()
| trkpt :: rest ->
-
(match (prev_time, Waypoint.get_time trkpt) 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 (Waypoint.get_time trkpt) rest
+
check_time_order (Waypoint.time trkpt) rest
in
check_time_order None points;
···
let location = "track" in
(* Check for empty track *)
-
let segments = Track.get_segments track in
+
let segments = Track.segments track in
if segments = [] then
issues := make_warning ~location "Track has no segments" :: !issues;
···
let issues = ref [] in
(* Check GPX version *)
-
let version = Gpx_doc.get_version gpx in
+
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)" version) :: !issues
···
"GPX 1.0 detected - consider upgrading to GPX 1.1 for better compatibility" :: !issues;
(* Check for empty creator *)
-
let creator = Gpx_doc.get_creator gpx in
+
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_doc.get_metadata gpx with
+
(match Doc.metadata gpx with
| Some metadata -> issues := validate_metadata metadata @ !issues
| None -> ());
(* Validate waypoints *)
-
let waypoints = Gpx_doc.get_waypoints gpx in
+
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
) waypoints;
(* Validate routes *)
-
let routes = Gpx_doc.get_routes gpx in
+
let routes = Doc.routes gpx in
List.iteri (fun _i route ->
issues := validate_route route @ !issues
) routes;
(* Validate tracks *)
-
let tracks = Gpx_doc.get_tracks gpx in
+
let tracks = Doc.tracks gpx in
List.iteri (fun _i track ->
issues := validate_track track @ !issues
) tracks;
···
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 -4
lib/gpx/validate.mli
···
}
(** Validate a complete GPX document *)
-
val validate_gpx : Gpx_doc.t -> validation_result
+
val validate_gpx : Doc.t -> validation_result
(** Quick validation - returns true if document is valid *)
-
val is_valid : Gpx_doc.t -> bool
+
val is_valid : Doc.t -> bool
(** Get only error messages *)
-
val get_errors : Gpx_doc.t -> validation_issue list
+
val errors : Doc.t -> validation_issue list
(** Get only warning messages *)
-
val get_warnings : Gpx_doc.t -> validation_issue list
+
val warnings : Doc.t -> validation_issue list
(** Format validation issue for display *)
val format_issue : validation_issue -> string
+39 -53
lib/gpx/waypoint.ml
···
| Error e, _ | _, Error e -> Error e
(** Get coordinate pair *)
-
let get_coordinate t = Coordinate.make t.lat t.lon
+
let coordinate t = Coordinate.make t.lat t.lon
(** Get latitude *)
-
let get_lat t = t.lat
+
let lat t = t.lat
(** Get longitude *)
-
let get_lon t = t.lon
+
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 get_elevation t = t.ele
+
let elevation t = t.ele
(** Get time *)
-
let get_time t = t.time
+
let time t = t.time
(** Get name *)
-
let get_name t = t.name
+
let name t = t.name
(** Get description *)
-
let get_description t = t.desc
+
let description t = t.desc
(** Get comment *)
-
let get_comment t = t.cmt
+
let comment t = t.cmt
(** Get source *)
-
let get_source t = t.src
+
let source t = t.src
(** Get symbol *)
-
let get_symbol t = t.sym
+
let symbol t = t.sym
(** Get type *)
-
let get_type t = t.type_
+
let type_ t = t.type_
(** Get fix type *)
-
let get_fix t = t.fix
+
let fix t = t.fix
(** Get satellite count *)
-
let get_sat t = t.sat
+
let sat t = t.sat
(** Get horizontal dilution of precision *)
-
let get_hdop t = t.hdop
+
let hdop t = t.hdop
(** Get vertical dilution of precision *)
-
let get_vdop t = t.vdop
+
let vdop t = t.vdop
(** Get position dilution of precision *)
-
let get_pdop t = t.pdop
+
let pdop t = t.pdop
(** Get magnetic variation *)
-
let get_magvar t = t.magvar
+
let magvar t = t.magvar
(** Get geoid height *)
-
let get_geoidheight t = t.geoidheight
+
let geoidheight t = t.geoidheight
(** Get age of DGPS data *)
-
let get_ageofdgpsdata t = t.ageofdgpsdata
+
let ageofdgpsdata t = t.ageofdgpsdata
(** Get DGPS ID *)
-
let get_dgpsid t = t.dgpsid
+
let dgpsid t = t.dgpsid
(** Get links *)
-
let get_links t = t.links
+
let links t = t.links
(** Get extensions *)
-
let get_extensions t = t.extensions
-
-
(** Set name *)
-
let set_name name t = { t with name = Some name }
-
-
(** Set description *)
-
let set_description desc t = { t with desc = Some desc }
-
-
(** Set elevation *)
-
let set_elevation ele t = { t with ele = Some ele }
-
-
(** Set time *)
-
let set_time time t = { t with time = Some time }
+
let extensions t = t.extensions
-
(** Functional setters for building waypoints *)
-
-
(** Set elevation *)
+
(** Update elevation *)
let with_elevation t ele = { t with ele = Some ele }
-
(** Set time *)
+
(** Update time *)
let with_time t time = { t with time }
-
(** Set name *)
+
(** Update name *)
let with_name t name = { t with name = Some name }
-
(** Set comment *)
+
(** Update comment *)
let with_comment t cmt = { t with cmt = Some cmt }
-
(** Set description *)
+
(** Update description *)
let with_description t desc = { t with desc = Some desc }
-
(** Set source *)
+
(** Update source *)
let with_source t src = { t with src = Some src }
-
(** Set symbol *)
+
(** Update symbol *)
let with_symbol t sym = { t with sym = Some sym }
-
(** Set type *)
+
(** Update type *)
let with_type t type_ = { t with type_ = Some type_ }
-
(** Set fix type *)
+
(** Update fix *)
let with_fix t fix = { t with fix }
-
(** Set satellite count *)
+
(** Update satellite count *)
let with_sat t sat = { t with sat = Some sat }
-
(** Set horizontal dilution of precision *)
+
(** Update HDOP *)
let with_hdop t hdop = { t with hdop = Some hdop }
-
(** Set vertical dilution of precision *)
+
(** Update VDOP *)
let with_vdop t vdop = { t with vdop = Some vdop }
-
(** Set position dilution of precision *)
+
(** Update PDOP *)
let with_pdop t pdop = { t with pdop = Some pdop }
-
(** Set magnetic variation *)
+
(** Update magnetic variation *)
let with_magvar t magvar = { t with magvar = Some magvar }
-
(** Set geoid height *)
+
(** Update geoid height *)
let with_geoidheight t geoidheight = { t with geoidheight = Some geoidheight }
-
(** Set age of DGPS data *)
+
(** Update age of DGPS data *)
let with_ageofdgpsdata t ageofdgpsdata = { t with ageofdgpsdata = Some ageofdgpsdata }
-
(** Set DGPS ID *)
+
(** Update DGPS ID *)
let with_dgpsid t dgpsid = { t with dgpsid = Some dgpsid }
(** Add link *)
+40 -52
lib/gpx/waypoint.mli
···
val make_from_floats : lat:float -> lon:float -> ?name:string -> ?desc:string -> unit -> (t, string) result
(** Get coordinate pair *)
-
val get_coordinate : t -> Coordinate.t
+
val coordinate : t -> Coordinate.t
(** Get latitude *)
-
val get_lat : t -> Coordinate.latitude
+
val lat : t -> Coordinate.latitude
(** Get longitude *)
-
val get_lon : t -> Coordinate.longitude
+
val lon : t -> Coordinate.longitude
(** Get coordinate as float pair *)
val to_floats : t -> float * float
(** Get elevation *)
-
val get_elevation : t -> float option
+
val elevation : t -> float option
(** Get time *)
-
val get_time : t -> Ptime.t option
+
val time : t -> Ptime.t option
(** Get name *)
-
val get_name : t -> string option
+
val name : t -> string option
(** Get description *)
-
val get_description : t -> string option
+
val description : t -> string option
(** Get comment *)
-
val get_comment : t -> string option
+
val comment : t -> string option
(** Get source *)
-
val get_source : t -> string option
+
val source : t -> string option
(** Get symbol *)
-
val get_symbol : t -> string option
+
val symbol : t -> string option
(** Get type *)
-
val get_type : t -> string option
+
val type_ : t -> string option
(** Get fix type *)
-
val get_fix : t -> fix_type option
+
val fix : t -> fix_type option
(** Get satellite count *)
-
val get_sat : t -> int option
+
val sat : t -> int option
(** Get horizontal dilution of precision *)
-
val get_hdop : t -> float option
+
val hdop : t -> float option
(** Get vertical dilution of precision *)
-
val get_vdop : t -> float option
+
val vdop : t -> float option
(** Get position dilution of precision *)
-
val get_pdop : t -> float option
+
val pdop : t -> float option
(** Get magnetic variation *)
-
val get_magvar : t -> Coordinate.degrees option
+
val magvar : t -> Coordinate.degrees option
(** Get geoid height *)
-
val get_geoidheight : t -> float option
+
val geoidheight : t -> float option
(** Get age of DGPS data *)
-
val get_ageofdgpsdata : t -> float option
+
val ageofdgpsdata : t -> float option
(** Get DGPS ID *)
-
val get_dgpsid : t -> int option
+
val dgpsid : t -> int option
(** Get links *)
-
val get_links : t -> Link.t list
+
val links : t -> Link.t list
(** Get extensions *)
-
val get_extensions : t -> Extension.t list
-
-
(** Set name *)
-
val set_name : string -> t -> t
-
-
(** Set description *)
-
val set_description : string -> t -> t
-
-
(** Set elevation *)
-
val set_elevation : float -> t -> t
+
val extensions : t -> Extension.t list
-
(** Set time *)
-
val set_time : Ptime.t -> t -> t
+
(** Functional operations for building waypoints *)
-
(** Functional setters for building waypoints *)
-
-
(** Set elevation *)
+
(** Update elevation *)
val with_elevation : t -> float -> t
-
(** Set time *)
+
(** Update time *)
val with_time : t -> Ptime.t option -> t
-
(** Set name *)
+
(** Update name *)
val with_name : t -> string -> t
-
(** Set comment *)
+
(** Update comment *)
val with_comment : t -> string -> t
-
(** Set description *)
+
(** Update description *)
val with_description : t -> string -> t
-
(** Set source *)
+
(** Update source *)
val with_source : t -> string -> t
-
(** Set symbol *)
+
(** Update symbol *)
val with_symbol : t -> string -> t
-
(** Set type *)
+
(** Update type *)
val with_type : t -> string -> t
-
(** Set fix type *)
+
(** Update fix *)
val with_fix : t -> fix_type option -> t
-
(** Set satellite count *)
+
(** Update satellite count *)
val with_sat : t -> int -> t
-
(** Set horizontal dilution of precision *)
+
(** Update HDOP *)
val with_hdop : t -> float -> t
-
(** Set vertical dilution of precision *)
+
(** Update VDOP *)
val with_vdop : t -> float -> t
-
(** Set position dilution of precision *)
+
(** Update PDOP *)
val with_pdop : t -> float -> t
-
(** Set magnetic variation *)
+
(** Update magnetic variation *)
val with_magvar : t -> Coordinate.degrees -> t
-
(** Set geoid height *)
+
(** Update geoid height *)
val with_geoidheight : t -> float -> t
-
(** Set age of DGPS data *)
+
(** Update age of DGPS data *)
val with_ageofdgpsdata : t -> float -> t
-
(** Set DGPS ID *)
+
(** Update DGPS ID *)
val with_dgpsid : t -> int -> t
(** Add link *)
+242 -29
lib/gpx/writer.ml
···
-
(** GPX XML writer using xmlm *)
+
(** GPX XML writer with complete spec coverage *)
(** Result binding operators *)
let (let*) = Result.bind
···
| Some text -> output_text_element writer name text
| None -> Ok ()
+
let output_optional_float_element writer name = function
+
| Some value -> output_text_element writer name (Printf.sprintf "%.6f" value)
+
| None -> Ok ()
+
+
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 value -> output_text_element writer name (string_of_int value)
+
| None -> Ok ()
+
+
let output_optional_time_element writer name = function
+
| Some time -> output_text_element writer name (Ptime.to_rfc3339 time)
+
| None -> 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 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 link) in
+
let* () = output_optional_text_element writer "type" (Link.type_ link) in
+
output_element_end writer
+
+
let output_links writer links =
+
let rec write_links = function
+
| [] -> Ok ()
+
| link :: rest ->
+
let* () = output_link writer link in
+
write_links rest
+
in
+
write_links links
+
+
(** 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
+
let* () = match Link.person_link person with
+
| Some link -> output_link writer link
+
| None -> Ok ()
+
in
+
output_element_end writer
+
+
(** 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* () = 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 element *)
+
let output_bounds writer bounds =
+
let (minlat, minlon, maxlat, maxlon) = Metadata.Bounds.bounds bounds in
+
let attrs = [
+
(("", "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 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 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 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
+
write_waypoints waypoints
+
+
(** 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
+
write_points points
+
+
(** 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
+
write_routes routes
+
+
(** 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
+
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
+
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 = Gpx_doc.get_version gpx in
-
let creator = Gpx_doc.get_creator gpx in
+
let version = Doc.version gpx in
+
let creator = Doc.creator gpx in
let attrs = [
(("", "version"), version);
(("", "creator"), creator);
···
let* () = output_element_start writer "gpx" attrs in
(* Write metadata if present *)
-
let* () = match Gpx_doc.get_metadata gpx with
-
| Some metadata ->
-
let* () = output_element_start writer "metadata" [] in
-
(* Write basic metadata fields *)
-
let* () = output_optional_text_element writer "name" (Metadata.get_name metadata) in
-
let* () = output_optional_text_element writer "desc" (Metadata.get_description metadata) in
-
let* () = output_optional_text_element writer "keywords" (Metadata.get_keywords metadata) in
-
output_element_end writer
-
| None -> Ok ()
+
let* () = match Doc.metadata gpx with
+
| Some metadata -> output_metadata writer metadata
+
| None -> Ok ()
in
(* Write waypoints *)
-
let waypoints = Gpx_doc.get_waypoints gpx in
-
let rec write_waypoints = function
-
| [] -> Ok ()
-
| wpt :: rest ->
-
let lat = Coordinate.latitude_to_float (Waypoint.get_lat wpt) in
-
let lon = Coordinate.longitude_to_float (Waypoint.get_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_optional_text_element writer "name" (Waypoint.get_name wpt) in
-
let* () = output_optional_text_element writer "desc" (Waypoint.get_description wpt) in
-
let* () = output_element_end writer in
-
write_waypoints rest
-
in
-
let* () = write_waypoints waypoints in
+
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
+2 -2
lib/gpx/writer.mli
···
(** GPX streaming writer using xmlm *)
(** Write a GPX document to an xmlm output destination *)
-
val write : ?validate:bool -> Xmlm.dest -> Gpx_doc.t -> (unit, Error.t) result
+
val write : ?validate:bool -> Xmlm.dest -> Doc.t -> (unit, Error.t) result
(** Write a GPX document to a string *)
-
val write_string : ?validate:bool -> Gpx_doc.t -> (string, Error.t) 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))
+6 -71
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 ?(validate=false) ~fs path = IO.read_file ~validate ~fs path
···
(** Write GPX to Eio sink *)
let to_sink ?(validate=false) sink gpx = IO.write_sink ~validate sink gpx
-
(** Create simple waypoint *)
-
let make_waypoint ~fs:_ ~lat ~lon ?name ?desc () =
-
match (Gpx.Coordinate.latitude lat, Gpx.Coordinate.longitude lon) with
-
| (Ok lat, Ok lon) ->
-
let wpt = Gpx.Waypoint.make lat lon in
-
Gpx.Waypoint.with_name wpt (Option.value name ~default:"") |>
-
fun wpt -> Gpx.Waypoint.with_description wpt (Option.value desc ~default:"")
-
| (Error e, _) | (_, Error e) -> failwith ("Invalid coordinate: " ^ e)
-
-
(** Create simple track from coordinate list *)
-
let make_track_from_coords ~fs:_ ~name coords =
-
Gpx.Track.make_from_coords ~name coords
-
-
(** Create simple route from coordinate list *)
-
let make_route_from_coords ~fs:_ ~name coords =
-
Gpx.Route.make_from_coords ~name coords
-
-
(** Extract coordinates from waypoints *)
-
let waypoint_coords wpt = Gpx.Waypoint.to_floats wpt
-
-
(** Extract coordinates from track *)
-
let track_coords trk = Gpx.Track.to_coords trk
-
-
(** Extract coordinates from route *)
-
let route_coords rte = Gpx.Route.to_coords rte
-
-
(** Count total points in GPX *)
-
let count_points gpx =
-
let waypoints = Gpx.Gpx_doc.get_waypoints gpx in
-
let routes = Gpx.Gpx_doc.get_routes gpx in
-
let tracks = Gpx.Gpx_doc.get_tracks gpx in
-
List.length waypoints +
-
List.fold_left (fun acc r -> acc + List.length (Gpx.Route.get_points r)) 0 routes +
-
List.fold_left (fun acc t -> acc + Gpx.Track.point_count t) 0 tracks
-
-
(** 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 waypoints = Gpx.Gpx_doc.get_waypoints gpx in
-
let routes = Gpx.Gpx_doc.get_routes gpx in
-
let tracks = Gpx.Gpx_doc.get_tracks gpx in
-
{
-
waypoint_count = List.length waypoints;
-
route_count = List.length routes;
-
track_count = List.length tracks;
-
total_points = count_points gpx;
-
has_elevation = List.exists (fun w -> Gpx.Waypoint.get_elevation w <> None) waypoints;
-
has_time = List.exists (fun w -> Gpx.Waypoint.get_time w <> None) waypoints;
-
}
-
(** 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: %b\\n" stats.has_elevation;
-
Printf.printf " Has Time: %b\\n" stats.has_time
+
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
+11 -74
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.
···
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 ~validate:true fs "output.gpx" gpx;
(* Read it back *)
let gpx2 = read ~validate:true fs "output.gpx" in
-
Printf.printf "Read %d waypoints\n" (List.length gpx2.waypoints)
+
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
···
@raises Gpx.Gpx_error on write failure *)
val to_sink : ?validate:bool -> [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.t -> 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.t
-
-
(** 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.t
-
-
(** 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.t
-
-
(** Extract coordinates from waypoint.
-
@param wpt Waypoint data
-
@return (latitude, longitude) as floats *)
-
val waypoint_coords : Gpx.Waypoint.t -> float * float
-
-
(** Extract coordinates from track.
-
@param track Track
-
@return List of (latitude, longitude) pairs *)
-
val track_coords : Gpx.Track.t -> (float * float) list
-
-
(** Extract coordinates from route.
-
@param route Route
-
@return List of (latitude, longitude) pairs *)
-
val route_coords : Gpx.Route.t -> (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.t -> 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.t -> gpx_stats
-
-
(** Print GPX statistics to stdout.
+
(** Print GPX statistics to sink.
+
@param sink Output sink
@param gpx GPX document *)
-
val print_stats : Gpx.t -> unit
+
val print_stats : [> Eio.Flow.sink_ty ] Eio.Resource.t -> Gpx.t -> unit
+1 -3
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 ?(validate=false) ~fs path =
let content = Eio.Path.load Eio.Path.(fs / path) in
···
Eio.Path.save ~create:(`Or_truncate 0o644) Eio.Path.(fs / path) backup_content
with _ -> () (* Ignore restore errors *)
);
-
raise err
+
raise err
+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))
+2 -112
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 IO module *)
module IO = Gpx_io
-
-
(* Re-export common types *)
open Gpx
(** Convenience functions for common operations *)
···
(** Write GPX to file with backup *)
let write_with_backup = IO.write_file_with_backup
-
(** Convert GPX to string *)
-
let to_string = write_string
-
-
(** Parse GPX from string *)
-
let from_string = parse_string
-
-
(** Quick validation check *)
-
let is_valid = is_valid
-
-
(** Get validation issues *)
-
let validate = validate_gpx
-
-
(** Create simple waypoint *)
-
let make_waypoint ~lat ~lon ?name ?desc () =
-
match (Coordinate.latitude lat, Coordinate.longitude lon) with
-
| (Ok lat, Ok lon) ->
-
let wpt = Waypoint.make lat lon in
-
let wpt = match name with Some n -> Waypoint.with_name wpt n | None -> wpt in
-
let wpt = match desc with Some d -> Waypoint.with_description wpt d | None -> wpt in
-
Ok wpt
-
| (Error e, _) | (_, Error e) -> Error (Gpx.Error.invalid_coordinate e)
-
-
(** Create simple track from coordinate list *)
-
let make_track_from_coords ~name coords =
-
let make_trkpt (lat, lon) =
-
match (Coordinate.latitude lat, Coordinate.longitude lon) with
-
| (Ok lat, Ok lon) -> Ok (Waypoint.make lat lon)
-
| (Error e, _) | (_, Error e) -> Error (Gpx.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
-
Ok (Track.make_from_coords ~name coords)
-
-
(** Create simple route from coordinate list *)
-
let make_route_from_coords ~name coords =
-
let make_rtept (lat, lon) =
-
match (Coordinate.latitude lat, Coordinate.longitude lon) with
-
| (Ok lat, Ok lon) -> Ok (Waypoint.make lat lon)
-
| (Error e, _) | (_, Error e) -> Error (Gpx.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 (Route.make_from_coords ~name coords)
-
-
(** Extract coordinates from waypoints *)
-
let waypoint_coords wpt = Waypoint.to_floats wpt
-
-
(** Extract coordinates from track *)
-
let track_coords track = Track.to_coords track
-
-
(** Extract coordinates from route *)
-
let route_coords route = Route.to_coords route
-
-
(** Count total points in GPX *)
-
let count_points gpx =
-
let waypoints = Gpx_doc.get_waypoints gpx in
-
let routes = Gpx_doc.get_routes gpx in
-
let tracks = Gpx_doc.get_tracks gpx in
-
List.length waypoints +
-
List.fold_left (fun acc r -> acc + List.length (Route.get_points r)) 0 routes +
-
List.fold_left (fun acc t -> acc + Track.point_count t) 0 tracks
-
-
(** 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 waypoints = Gpx_doc.get_waypoints gpx in
-
let routes = Gpx_doc.get_routes gpx in
-
let tracks = Gpx_doc.get_tracks gpx in
-
{
-
waypoint_count = List.length waypoints;
-
route_count = List.length routes;
-
track_count = List.length tracks;
-
total_points = count_points gpx;
-
has_elevation = List.exists (fun w -> Waypoint.get_elevation w <> None) waypoints;
-
has_time = List.exists (fun w -> Waypoint.get_time w <> None) waypoints;
-
}
-
(** 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
+2 -54
lib/gpx_unix/gpx_unix.mli
···
-
(** High-level Unix API for GPX operations *)
+
(** Unix API for GPX operations *)
-
(* Re-export IO module *)
-
module IO = Gpx_io
-
-
(* Re-export common types *)
open Gpx
-
(** Convenience functions for common operations *)
-
(** Read and parse GPX file *)
val read : ?validate:bool -> string -> (t, error) result
···
(** Write GPX to file with backup *)
val write_with_backup : ?validate:bool -> string -> t -> (string, error) result
-
(** Convert GPX to string *)
-
val to_string : ?validate:bool -> t -> (string, error) result
-
-
(** Parse GPX from string *)
-
val from_string : ?validate:bool -> string -> (t, error) result
-
-
(** Quick validation check *)
-
val is_valid : t -> bool
-
-
(** Get validation issues *)
-
val validate : t -> validation_result
-
-
(** Create simple waypoint *)
-
val make_waypoint : lat:float -> lon:float -> ?name:string -> ?desc:string -> unit -> (Waypoint.t, error) result
-
-
(** Create simple track from coordinate list *)
-
val make_track_from_coords : name:string -> (float * float) list -> (Track.t, error) result
-
-
(** Create simple route from coordinate list *)
-
val make_route_from_coords : name:string -> (float * float) list -> (Route.t, error) result
-
-
(** Extract coordinates from waypoints *)
-
val waypoint_coords : Waypoint.t -> float * float
-
-
(** Extract coordinates from track *)
-
val track_coords : Track.t -> (float * float) list
-
-
(** Extract coordinates from route *)
-
val route_coords : Route.t -> (float * float) list
-
-
(** Count total points in GPX *)
-
val count_points : t -> 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 : t -> gpx_stats
-
(** Pretty print GPX statistics *)
-
val print_stats : t -> unit
+
val print_stats : t -> unit
+4 -1
mlgpx.opam
···
"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"
+
"eio" {>= "1.2"}
"ppx_expect"
"alcotest"
"eio_main"
···
]
]
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 -2
test/dune
···
(modules test_gpx))
;; ppx_expect inline tests
+
(library
(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))
+53 -50
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 ->
-
let waypoints = Gpx_doc.get_waypoints gpx in
+
let waypoints = Doc.waypoints gpx in
Printf.printf "Waypoints count: %d\n" (List.length waypoints);
Printf.printf "First waypoint name: %s\n"
(match waypoints with
-
| wpt :: _ -> (match Waypoint.get_name wpt with Some n -> n | None -> "None")
+
| wpt :: _ -> (match Waypoint.name wpt with Some n -> n | None -> "None")
| [] -> "None");
-
Printf.printf "Creator: %s\n" (Gpx_doc.get_creator gpx);
+
Printf.printf "Creator: %s\n" (Doc.creator gpx);
[%expect {|
Waypoints count: 3
First waypoint name: San Francisco
···
let content = read_test_file "detailed_waypoints.gpx" in
match parse_string content with
| Ok gpx ->
-
let waypoints = Gpx_doc.get_waypoints gpx in
-
let metadata = Gpx_doc.get_metadata gpx in
+
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 metadata with Some md -> Metadata.get_time md <> None | None -> false);
+
(match metadata with Some md -> Metadata.time md <> None | None -> false);
Printf.printf "Has bounds: %b\n"
-
(match metadata with Some md -> Metadata.get_bounds md <> None | None -> false);
+
(match metadata with Some md -> Metadata.bounds_opt md <> None | None -> false);
(match waypoints with
| wpt :: _ ->
-
Printf.printf "First waypoint has elevation: %b\n" (Waypoint.get_elevation wpt <> None);
-
Printf.printf "First waypoint has time: %b\n" (Waypoint.get_time wpt <> None);
-
Printf.printf "First waypoint has links: %b\n" (Waypoint.get_links wpt <> [])
+
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 ->
-
let routes = Gpx_doc.get_routes gpx in
+
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 Route.get_name rte with Some n -> n | None -> "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" false (* TODO: add get_number to Route *)
+
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 ->
-
let tracks = Gpx_doc.get_tracks gpx in
+
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 Track.get_name trk with Some n -> n | None -> "None");
+
(match Track.name trk with Some n -> n | None -> "None");
Printf.printf "Track segments: %d\n" (Track.segment_count trk);
-
let segments = Track.get_segments trk in
+
let segments = Track.segments trk in
(match segments with
| seg :: _ ->
Printf.printf "First segment points: %d\n" (Track.Segment.point_count seg);
-
let points = Track.Segment.get_points seg in
+
let points = Track.Segment.points seg in
(match points with
| pt :: _ ->
-
Printf.printf "First point has elevation: %b\n" (Waypoint.get_elevation pt <> None);
-
Printf.printf "First point has time: %b\n" (Waypoint.get_time pt <> 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 ->
-
let tracks = Gpx_doc.get_tracks gpx in
+
let tracks = Doc.tracks gpx in
Printf.printf "Tracks count: %d\n" (List.length tracks);
(match tracks with
| trk :: _ ->
···
let content = read_test_file "comprehensive.gpx" in
match parse_string content with
| Ok gpx ->
-
let waypoints = Gpx_doc.get_waypoints gpx in
-
let routes = Gpx_doc.get_routes gpx in
-
let tracks = Gpx_doc.get_tracks gpx in
-
let metadata = Gpx_doc.get_metadata gpx in
+
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 metadata with Some md -> Metadata.get_author md <> None | None -> false);
+
(match metadata with Some md -> Metadata.author md <> None | None -> false);
Printf.printf "Has copyright: %b\n"
-
(match metadata with Some md -> Metadata.get_copyright md <> None | None -> false);
+
(match metadata with Some md -> Metadata.copyright md <> None | None -> false);
Printf.printf "Has keywords: %b\n"
-
(match metadata with Some md -> Metadata.get_keywords md <> 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";
-
let waypoints = Gpx_doc.get_waypoints gpx in
-
let routes = Gpx_doc.get_routes gpx in
-
let tracks = Gpx_doc.get_tracks gpx in
+
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);
···
match parse_string content with
| Ok gpx ->
Printf.printf "Edge cases parsed successfully\n";
-
let waypoints = Gpx_doc.get_waypoints gpx in
-
let tracks = Gpx_doc.get_tracks gpx in
+
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 *)
···
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";
-
let waypoints = Gpx_doc.get_waypoints gpx in
-
let waypoints2 = Gpx_doc.get_waypoints gpx2 in
+
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" (Gpx_doc.get_creator gpx = Gpx_doc.get_creator gpx2);
-
[%expect {|
-
Round-trip successful
-
Original waypoints: 3
-
Round-trip waypoints: 3
-
Creators match: true |}]
+
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]
+15 -15
test/test_corpus_unix_eio.ml
···
(** Helper to compare GPX documents *)
let compare_gpx_basic gpx1 gpx2 =
let open Gpx in
-
Gpx_doc.get_creator gpx1 = Gpx_doc.get_creator gpx2 &&
-
List.length (Gpx_doc.get_waypoints gpx1) = List.length (Gpx_doc.get_waypoints gpx2) &&
-
List.length (Gpx_doc.get_routes gpx1) = List.length (Gpx_doc.get_routes gpx2) &&
-
List.length (Gpx_doc.get_tracks gpx1) = List.length (Gpx_doc.get_tracks gpx2)
+
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.Gpx_doc.get_waypoints gpx) > 0 ||
-
List.length (Gpx.Gpx_doc.get_routes gpx) > 0 ||
-
List.length (Gpx.Gpx_doc.get_tracks gpx) > 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 (Gpx.Error.to_string err)
···
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.Gpx_doc.get_waypoints gpx) > 0 ||
-
List.length (Gpx.Gpx_doc.get_routes gpx) > 0 ||
-
List.length (Gpx.Gpx_doc.get_tracks gpx) > 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 ->
···
| 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.Gpx_doc.get_creator gpx_unix) (Gpx.Gpx_doc.get_creator gpx_eio);
+
check string "Creators match" (Gpx.Doc.creator gpx_unix) (Gpx.Doc.creator gpx_eio);
check int "Waypoint counts match"
-
(List.length (Gpx.Gpx_doc.get_waypoints gpx_unix)) (List.length (Gpx.Gpx_doc.get_waypoints gpx_eio));
+
(List.length (Gpx.Doc.waypoints gpx_unix)) (List.length (Gpx.Doc.waypoints gpx_eio));
check int "Route counts match"
-
(List.length (Gpx.Gpx_doc.get_routes gpx_unix)) (List.length (Gpx.Gpx_doc.get_routes gpx_eio));
+
(List.length (Gpx.Doc.routes gpx_unix)) (List.length (Gpx.Doc.routes gpx_eio));
check int "Track counts match"
-
(List.length (Gpx.Gpx_doc.get_tracks gpx_unix)) (List.length (Gpx.Gpx_doc.get_tracks gpx_eio))
+
(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.Gpx_doc.get_creator gpx_original) (Gpx.Gpx_doc.get_creator gpx_roundtrip)
+
(Gpx.Doc.creator gpx_original) (Gpx.Doc.creator gpx_roundtrip)
| Error _ ->
failf "Round-trip parse failed for %s" filename)
| Error _ ->
+11 -12
test/test_gpx.ml
···
let test_gpx_creation () =
let creator = "test" in
-
let gpx = Gpx_doc.empty ~creator in
-
assert (Gpx_doc.get_creator gpx = creator);
-
assert (Gpx_doc.get_version gpx = "1.1");
-
assert (Gpx_doc.get_waypoints gpx = []);
+
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_doc.get_creator gpx = "test");
-
let waypoints = Gpx_doc.get_waypoints gpx in
+
assert (Doc.creator gpx = "test");
+
let waypoints = Doc.waypoints gpx in
assert (List.length waypoints = 1);
let wpt = List.hd waypoints in
-
assert (Waypoint.get_name wpt = Some "San Francisco");
+
assert (Waypoint.name wpt = Some "San Francisco");
Printf.printf "โœ“ Simple parsing tests passed\n"
| Error e ->
Printf.printf "โœ— Parsing failed: %s\n" (Error.to_string e);
···
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 = Waypoint.with_name wpt "Test Point" in
-
let wpt = Waypoint.with_description wpt "A test waypoint" in
-
let gpx = Gpx_doc.empty ~creator:"test" in
-
let gpx = Gpx_doc.add_waypoint gpx wpt 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 false
let test_validation () =
-
let gpx = Gpx_doc.empty ~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