GPS Exchange Format library/CLI in OCaml

refactor: deduplicate functions between platform modules and core

Move Statistics to Core:
- Add Doc.stats type and function (comprehensive GPX statistics)
- Add Doc.pp_stats for pretty-printing statistics using Format
- Remove duplicate stats implementations from Gpx_eio and Gpx_unix

Remove Function Duplication:
- Remove redundant accessor functions (waypoint_coords, track_coords, route_coords)
- Remove duplicate constructor functions (make_track_from_coords, make_route_from_coords)
- Remove duplicate count_points implementations
- Users should call core functions directly (Waypoint.to_floats, Track.to_coords, etc.)

Simplify Platform APIs:
- Platform modules now only contain I/O operations and print_stats
- Gpx_eio.print_stats and Gpx_unix.print_stats use Doc.pp_stats
- Removed unused Result binding operators
- Cleaner, more focused platform-specific functionality

This eliminates code duplication while keeping I/O operations
appropriately separated in platform-specific modules.

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

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

Changed files
+14 -241
lib
+8 -12
lib/gpx/doc.ml
···
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 *)
···
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 = 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")
+4 -4
lib/gpx/doc.mli
···
(** Get document statistics *)
val stats : t -> stats
+
(** Pretty print statistics *)
+
val pp_stats : Format.formatter -> t -> unit
+
(** {2 Comparison and Utilities} *)
(** Compare documents *)
···
val equal : t -> t -> bool
(** Pretty print document *)
-
val pp : Format.formatter -> t -> unit
-
-
(** Print document statistics to stdout *)
-
val print_stats : t -> unit
+
val pp : Format.formatter -> t -> unit
+1 -57
lib/gpx_eio/gpx_eio.ml
···
{ wpt with name; desc }
| (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.Doc.waypoints gpx in
-
let routes = Gpx.Doc.routes gpx in
-
let tracks = Gpx.Doc.tracks gpx in
-
List.length waypoints +
-
List.fold_left (fun acc r -> acc + List.length (Gpx.Route.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 stats gpx =
-
let waypoints = Gpx.Doc.waypoints gpx in
-
let routes = Gpx.Doc.routes gpx in
-
let tracks = Gpx.Doc.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.elevation w <> None) waypoints;
-
has_time = List.exists (fun w -> Gpx.Waypoint.time w <> None) waypoints;
-
}
-
(** Pretty print GPX statistics *)
let print_stats gpx =
-
let stats = 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
+
Format.printf "%a@." Gpx.Doc.pp_stats gpx
-51
lib/gpx_eio/gpx_eio.mli
···
@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 stats : Gpx.t -> gpx_stats
-
(** Print GPX statistics to stdout.
@param gpx GPX document *)
val print_stats : Gpx.t -> unit
+1 -86
lib/gpx_unix/gpx_unix.ml
···
(** High-level Unix API for GPX operations *)
-
(** Result binding operators *)
-
let (let*) = Result.bind
-
(* Re-export IO module *)
module IO = Gpx_io
···
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 = Doc.waypoints gpx in
-
let routes = Doc.routes gpx in
-
let tracks = Doc.tracks gpx in
-
List.length waypoints +
-
List.fold_left (fun acc r -> acc + List.length (Route.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 stats gpx =
-
let waypoints = Doc.waypoints gpx in
-
let routes = Doc.routes gpx in
-
let tracks = Doc.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.elevation w <> None) waypoints;
-
has_time = List.exists (fun w -> Waypoint.time w <> None) waypoints;
-
}
-
(** Pretty print GPX statistics *)
let print_stats gpx =
-
let stats = 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
-31
lib/gpx_unix/gpx_unix.mli
···
(** 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 stats : t -> gpx_stats
-
(** Pretty print GPX statistics *)
val print_stats : t -> unit