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))
+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.

+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 doc))
+
(modules
+
gpx
+
parser
+
writer
+
validate
+
coordinate
+
link
+
extension
+
waypoint
+
metadata
+
route
+
track
+
error
+
doc))
+2 -2
lib/gpx/gpx.mli
···
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
+
- {!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
···
- {{: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} *)
+
- {{:https://en.wikipedia.org/wiki/World_Geodetic_System}WGS84 Coordinate System} *)
+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 -14
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
-
{ wpt with name; desc }
-
| (Error e, _) | (_, Error e) -> failwith ("Invalid coordinate: " ^ e)
-
(** Pretty print GPX statistics *)
-
let print_stats gpx =
-
Format.printf "%a@." Gpx.Doc.pp_stats gpx
+
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 -23
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
-
-
(** 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 -26
lib/gpx_unix/gpx_unix.ml
···
-
(** 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 *)
···
(** 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 = { wpt with name; desc } in
-
Ok wpt
-
| (Error e, _) | (_, Error e) -> Error (Gpx.Error.invalid_coordinate e)
-
(** Pretty print GPX statistics *)
let print_stats gpx =
-
Format.printf "%a@." Doc.pp_stats gpx
+
Format.printf "%a@." Doc.pp_stats gpx
+2 -23
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
-
(** 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))