GPS Exchange Format library/CLI in OCaml

Add mlgpx CLI with GPX 1.0 support and enhanced info display

- Add comprehensive CLI tool with cmdliner and Eio backend
- Implement waypoint-to-trackset conversion with sorting options
- Add info command with detailed GPX analysis and time range display
- Add GPX 1.0 support alongside existing GPX 1.1 support
- Add ANSI color output with terminal detection using fmt library
- Update documentation with CLI usage examples
- Rename branding from 'MLGpx' to 'mlgpx' throughout

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+62 -1
README.md
···
## Architecture Overview
-
The library is split into three main components:
### Core Library (`gpx`)
- **Portable**: No Unix dependencies, works with js_of_ocaml
···
- **Effects-style API**: Similar to Eio patterns but using standard Unix I/O
- **Resource-safe**: Automatic file handle management
- **High-level**: Convenient functions for common operations
## Key Features
···
Printf.eprintf "GPX Error: %s\n" (Gpx.error_to_string err)
let () = create_simple_gpx ()
```
## Dependencies
···
## Architecture Overview
+
The library is split into four main components:
### Core Library (`gpx`)
- **Portable**: No Unix dependencies, works with js_of_ocaml
···
- **Effects-style API**: Similar to Eio patterns but using standard Unix I/O
- **Resource-safe**: Automatic file handle management
- **High-level**: Convenient functions for common operations
+
+
### Command Line Interface (`mlgpx`)
+
- **Unix-style CLI**: Built with cmdliner for proper argument parsing
+
- **Eio-powered**: Uses Eio backend for efficient I/O operations
+
- **Waypoint conversion**: Convert waypoints to tracksets with sorting options
+
- **File analysis**: Inspect GPX files with detailed information display
## Key Features
···
Printf.eprintf "GPX Error: %s\n" (Gpx.error_to_string err)
let () = create_simple_gpx ()
+
```
+
+
## Command Line Usage
+
+
The `mlgpx` CLI provides tools for manipulating GPX files from the command line.
+
+
### Installation
+
+
```bash
+
# Install from source
+
dune build @install
+
dune install
+
+
# Or use opam (when published)
+
opam install mlgpx
+
```
+
+
### Convert Waypoints to Track
+
+
```bash
+
# Basic conversion
+
mlgpx convert waypoints.gpx track.gpx
+
+
# With custom track name
+
mlgpx convert --name "My Route" waypoints.gpx route.gpx
+
+
# Sort waypoints by timestamp before conversion
+
mlgpx convert --sort-time waypoints.gpx sorted_track.gpx
+
+
# Sort by name and preserve original waypoints
+
mlgpx convert --sort-name --preserve waypoints.gpx mixed.gpx
+
+
# Verbose output with description
+
mlgpx convert --verbose --desc "Generated route" waypoints.gpx track.gpx
+
```
+
+
### File Analysis
+
+
```bash
+
# Basic file information
+
mlgpx info file.gpx
+
+
# Detailed analysis with waypoint details
+
mlgpx info --verbose file.gpx
+
```
+
+
### Help
+
+
```bash
+
# General help
+
mlgpx --help
+
+
# Command-specific help
+
mlgpx convert --help
+
mlgpx info --help
```
## Dependencies
+4
bin/dune
···
···
+
(executable
+
(public_name mlgpx)
+
(name mlgpx_cli)
+
(libraries gpx gpx_eio cmdliner eio_main fmt fmt.tty fmt.cli))
+466
bin/mlgpx_cli.ml
···
···
+
(** 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
+
let track_points = List.map (fun (wpt : waypoint) -> (wpt :> track_point)) waypoints in
+
[{ trkpts = track_points; extensions = [] }]
+
+
let sort_waypoints sort_by_time sort_by_name waypoints =
+
if sort_by_time then
+
List.sort (fun (wpt1 : waypoint) (wpt2 : waypoint) ->
+
match wpt1.time, wpt2.time with
+
| Some t1, Some t2 -> Ptime.compare t1 t2
+
| Some _, None -> -1
+
| None, Some _ -> 1
+
| None, None -> 0
+
) waypoints
+
else if sort_by_name then
+
List.sort (fun (wpt1 : waypoint) (wpt2 : waypoint) ->
+
match wpt1.name, wpt2.name with
+
| 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"
+
(List.length gpx.waypoints)
+
(List.length gpx.tracks);
+
+
(* Check if we have waypoints to convert *)
+
if gpx.waypoints = [] then (
+
log_error "Input file contains no waypoints - nothing to convert";
+
exit 1
+
);
+
+
(* Sort waypoints if requested *)
+
let sorted_waypoints = sort_waypoints sort_by_time sort_by_name gpx.waypoints in
+
+
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 = {
+
name = Some track_name;
+
cmt = Some "Generated from waypoints by mlgpx CLI";
+
desc = track_desc;
+
src = Some "mlgpx";
+
links = [];
+
number = None;
+
type_ = Some "converted";
+
extensions = [];
+
trksegs = track_segments;
+
} in
+
+
if verbose then (
+
let total_points = List.fold_left (fun acc seg -> acc + List.length seg.trkpts) 0 track_segments in
+
log_info "Created track %a with %d segments containing %d points"
+
(bold_style Fmt.string) track_name
+
(List.length track_segments) total_points
+
);
+
+
(* Build output GPX *)
+
let output_gpx = {
+
gpx with
+
waypoints = (if preserve_waypoints then gpx.waypoints else []);
+
tracks = new_track :: gpx.tracks;
+
metadata = (match gpx.metadata with
+
| Some meta -> Some { meta with
+
desc = Some (match meta.desc with
+
| Some existing -> existing ^ " (waypoints converted to track)"
+
| None -> "Waypoints converted to track") }
+
| None -> Some { empty_metadata with
+
desc = Some "Waypoints converted to track";
+
time = None })
+
} in
+
+
(* Validate output *)
+
let validation = validate_gpx output_gpx in
+
if not validation.is_valid then (
+
log_error "Generated GPX failed validation:";
+
List.iter (fun 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"
+
(List.length output_gpx.waypoints)
+
(if preserve_waypoints then " (preserved)" else " (removed)");
+
Fmt.pf Format.err_formatter " - %d tracks (%a + %d existing)\n"
+
(List.length output_gpx.tracks)
+
(success_style Fmt.string) "1 new"
+
(List.length gpx.tracks)
+
) 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" (match err with
+
| Invalid_xml s -> "Invalid XML: " ^ s
+
| Invalid_coordinate s -> "Invalid coordinate: " ^ s
+
| Missing_required_attribute (elem, attr) ->
+
Printf.sprintf "Missing attribute %s in %s" attr elem
+
| Missing_required_element s -> "Missing element: " ^ s
+
| Validation_error s -> "Validation error: " ^ s
+
| Xml_error s -> "XML error: " ^ s
+
| IO_error s -> "I/O error: " ^ s);
+
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 : waypoint) ->
+
match wpt.time with
+
| Some t -> times := t :: !times
+
| None -> ()
+
) gpx.waypoints;
+
+
(* Collect from routes *)
+
List.iter (fun route ->
+
List.iter (fun (rtept : route_point) ->
+
match rtept.time with
+
| Some t -> times := t :: !times
+
| None -> ()
+
) route.rtepts
+
) gpx.routes;
+
+
(* Collect from tracks *)
+
List.iter (fun track ->
+
List.iter (fun seg ->
+
List.iter (fun (trkpt : track_point) ->
+
match trkpt.time with
+
| Some t -> times := t :: !times
+
| None -> ()
+
) seg.trkpts
+
) track.trksegs
+
) gpx.tracks;
+
+
!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" gpx.version;
+
Printf.printf " Creator: %s\n" gpx.creator;
+
+
(match gpx.metadata with
+
| Some meta ->
+
Printf.printf " Name: %s\n" (Option.value meta.name ~default:"<unnamed>");
+
Printf.printf " Description: %s\n" (Option.value meta.desc ~default:"<none>");
+
(match meta.time with
+
| 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" (List.length gpx.waypoints);
+
Printf.printf " Routes: %d\n" (List.length gpx.routes);
+
Printf.printf " Tracks: %d\n" (List.length gpx.tracks);
+
+
(* 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 gpx.waypoints <> [] then (
+
Fmt.pf Format.std_formatter "\n%a\n" (bold_style Fmt.string) "Waypoints";
+
let waypoints_with_time = List.filter (fun (wpt : waypoint) -> wpt.time <> None) gpx.waypoints in
+
let waypoints_with_elevation = List.filter (fun (wpt : waypoint) -> wpt.ele <> None) gpx.waypoints in
+
Printf.printf " - %d with timestamps\n" (List.length waypoints_with_time);
+
Printf.printf " - %d with elevation data\n" (List.length waypoints_with_elevation);
+
+
if verbose && List.length gpx.waypoints <= 10 then (
+
Printf.printf " Details:\n";
+
List.iteri (fun i (wpt : waypoint) ->
+
Fmt.pf Format.std_formatter " %a %s (%.6f, %.6f)%s%s\n"
+
(info_style Fmt.string) (Printf.sprintf "%d." (i + 1))
+
(Option.value wpt.name ~default:"<unnamed>")
+
(latitude_to_float wpt.lat) (longitude_to_float wpt.lon)
+
(match wpt.ele with Some e -> Printf.sprintf " elev=%.1fm" e | None -> "")
+
(match wpt.time with Some t -> " @" ^ Ptime.to_rfc3339 t | None -> "")
+
) gpx.waypoints
+
)
+
);
+
+
(* Track info *)
+
if gpx.tracks <> [] then (
+
Fmt.pf Format.std_formatter "\n%a\n" (bold_style Fmt.string) "Tracks";
+
List.iteri (fun i track ->
+
let total_points = List.fold_left (fun acc seg -> acc + List.length seg.trkpts) 0 track.trksegs in
+
Fmt.pf Format.std_formatter " %a %s (%d segments, %d points)\n"
+
(info_style Fmt.string) (Printf.sprintf "%d." (i + 1))
+
(Option.value track.name ~default:"<unnamed>")
+
(List.length track.trksegs) total_points
+
) gpx.tracks
+
);
+
+
(* Validation *)
+
let validation = 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 ->
+
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" (match err with
+
| Invalid_xml s -> "Invalid XML: " ^ s
+
| Invalid_coordinate s -> "Invalid coordinate: " ^ s
+
| Missing_required_attribute (elem, attr) ->
+
Printf.sprintf "Missing attribute %s in %s" attr elem
+
| Missing_required_element s -> "Missing element: " ^ s
+
| Validation_error s -> "Validation error: " ^ s
+
| Xml_error s -> "XML error: " ^ s
+
| IO_error s -> "I/O error: " ^ s);
+
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)
+1 -1
dune-project
···
(package
(name mlgpx)
-
(depends ocaml dune xmlm ptime eio ppx_expect alcotest eio_main)
(synopsis "OCaml library for parsing and generating GPX files")
(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.")
···
(package
(name mlgpx)
+
(depends ocaml dune xmlm ptime eio ppx_expect alcotest eio_main cmdliner fmt logs)
(synopsis "OCaml library for parsing and generating GPX files")
(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.")
+1 -1
examples/simple_gpx.ml
···
open Gpx
let () =
-
Printf.printf "=== MLGpx Library Example ===\n\n";
(* Create coordinates using direct API *)
let create_coordinate_pair lat_f lon_f =
···
open Gpx
let () =
+
Printf.printf "=== mlgpx Library Example ===\n\n";
(* Create coordinates using direct API *)
let create_coordinate_pair lat_f lon_f =
+1 -1
lib/gpx/gpx.ml
···
-
(** {1 MLGpx - OCaml GPX Library} *)
(** Core type definitions and utilities *)
module Types = Types
···
+
(** {1 mlgpx - OCaml GPX Library} *)
(** Core type definitions and utilities *)
module Types = Types
+2 -2
lib/gpx/parser.ml
···
parser.current_element <- ["gpx"];
let* version = require_attribute "version" attrs "gpx" in
let* creator = require_attribute "creator" attrs "gpx" in
-
if version <> "1.1" then
-
Error (Validation_error ("Unsupported GPX version: " ^ version))
else
Ok (version, creator)
| `El_start _ ->
···
parser.current_element <- ["gpx"];
let* version = require_attribute "version" attrs "gpx" in
let* creator = require_attribute "creator" attrs "gpx" in
+
if version <> "1.0" && version <> "1.1" then
+
Error (Validation_error ("Unsupported GPX version: " ^ version ^ " (supported: 1.0, 1.1)"))
else
Ok (version, creator)
| `El_start _ ->
+1 -1
lib/gpx/types.ml
···
(** Main GPX document *)
type gpx = {
-
version : string; (* Always "1.1" for this version *)
creator : string;
metadata : metadata option;
waypoints : waypoint list;
···
(** Main GPX document *)
type gpx = {
+
version : string; (* GPX version: "1.0" or "1.1" *)
creator : string;
metadata : metadata option;
waypoints : waypoint list;
+5 -2
lib/gpx/validate.ml
···
let issues = ref [] in
(* Check GPX version *)
-
if gpx.version <> "1.1" then
issues := make_error ~location:"gpx"
-
(Printf.sprintf "Unsupported GPX version: %s" gpx.version) :: !issues;
(* Check for empty creator *)
if String.trim gpx.creator = "" then
···
let issues = ref [] in
(* Check GPX version *)
+
if gpx.version <> "1.0" && gpx.version <> "1.1" then
issues := make_error ~location:"gpx"
+
(Printf.sprintf "Unsupported GPX version: %s (supported: 1.0, 1.1)" gpx.version) :: !issues
+
else if gpx.version = "1.0" then
+
issues := make_warning ~location:"gpx"
+
"GPX 1.0 detected - consider upgrading to GPX 1.1 for better compatibility" :: !issues;
(* Check for empty creator *)
if String.trim gpx.creator = "" then