this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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>

+543 -9
+62 -1
README.md
··· 4 4 5 5 ## Architecture Overview 6 6 7 - The library is split into three main components: 7 + The library is split into four main components: 8 8 9 9 ### Core Library (`gpx`) 10 10 - **Portable**: No Unix dependencies, works with js_of_ocaml ··· 23 23 - **Effects-style API**: Similar to Eio patterns but using standard Unix I/O 24 24 - **Resource-safe**: Automatic file handle management 25 25 - **High-level**: Convenient functions for common operations 26 + 27 + ### Command Line Interface (`mlgpx`) 28 + - **Unix-style CLI**: Built with cmdliner for proper argument parsing 29 + - **Eio-powered**: Uses Eio backend for efficient I/O operations 30 + - **Waypoint conversion**: Convert waypoints to tracksets with sorting options 31 + - **File analysis**: Inspect GPX files with detailed information display 26 32 27 33 ## Key Features 28 34 ··· 220 226 Printf.eprintf "GPX Error: %s\n" (Gpx.error_to_string err) 221 227 222 228 let () = create_simple_gpx () 229 + ``` 230 + 231 + ## Command Line Usage 232 + 233 + The `mlgpx` CLI provides tools for manipulating GPX files from the command line. 234 + 235 + ### Installation 236 + 237 + ```bash 238 + # Install from source 239 + dune build @install 240 + dune install 241 + 242 + # Or use opam (when published) 243 + opam install mlgpx 244 + ``` 245 + 246 + ### Convert Waypoints to Track 247 + 248 + ```bash 249 + # Basic conversion 250 + mlgpx convert waypoints.gpx track.gpx 251 + 252 + # With custom track name 253 + mlgpx convert --name "My Route" waypoints.gpx route.gpx 254 + 255 + # Sort waypoints by timestamp before conversion 256 + mlgpx convert --sort-time waypoints.gpx sorted_track.gpx 257 + 258 + # Sort by name and preserve original waypoints 259 + mlgpx convert --sort-name --preserve waypoints.gpx mixed.gpx 260 + 261 + # Verbose output with description 262 + mlgpx convert --verbose --desc "Generated route" waypoints.gpx track.gpx 263 + ``` 264 + 265 + ### File Analysis 266 + 267 + ```bash 268 + # Basic file information 269 + mlgpx info file.gpx 270 + 271 + # Detailed analysis with waypoint details 272 + mlgpx info --verbose file.gpx 273 + ``` 274 + 275 + ### Help 276 + 277 + ```bash 278 + # General help 279 + mlgpx --help 280 + 281 + # Command-specific help 282 + mlgpx convert --help 283 + mlgpx info --help 223 284 ``` 224 285 225 286 ## Dependencies
+4
bin/dune
··· 1 + (executable 2 + (public_name mlgpx) 3 + (name mlgpx_cli) 4 + (libraries gpx gpx_eio cmdliner eio_main fmt fmt.tty fmt.cli))
+466
bin/mlgpx_cli.ml
··· 1 + (** mlgpx Command Line Interface with pretty ANSI output *) 2 + 3 + open Cmdliner 4 + open Gpx 5 + 6 + (* Terminal and formatting setup *) 7 + let setup_fmt style_renderer = 8 + Fmt_tty.setup_std_outputs ?style_renderer (); 9 + () 10 + 11 + (* Color formatters *) 12 + let info_style = Fmt.(styled (`Fg `Green)) 13 + let warn_style = Fmt.(styled (`Fg `Yellow)) 14 + let error_style = Fmt.(styled (`Fg `Red)) 15 + let success_style = Fmt.(styled (`Fg `Green)) 16 + let bold_style = Fmt.(styled `Bold) 17 + 18 + (* Logging functions *) 19 + let log_info fmt = 20 + Fmt.pf Format.err_formatter "[%a] " (info_style Fmt.string) "INFO"; 21 + Format.kfprintf (fun fmt -> Format.pp_print_newline fmt (); Format.pp_print_flush fmt ()) Format.err_formatter fmt 22 + 23 + 24 + let log_error fmt = 25 + Fmt.pf Format.err_formatter "[%a] " (error_style Fmt.string) "ERROR"; 26 + Format.kfprintf (fun fmt -> Format.pp_print_newline fmt (); Format.pp_print_flush fmt ()) Format.err_formatter fmt 27 + 28 + let log_success fmt = 29 + Format.kfprintf (fun fmt -> Format.pp_print_newline fmt (); Format.pp_print_flush fmt ()) Format.std_formatter fmt 30 + 31 + (* Utility functions *) 32 + let waypoints_to_track_segments waypoints = 33 + if waypoints = [] then 34 + [] 35 + else 36 + let track_points = List.map (fun (wpt : waypoint) -> (wpt :> track_point)) waypoints in 37 + [{ trkpts = track_points; extensions = [] }] 38 + 39 + let sort_waypoints sort_by_time sort_by_name waypoints = 40 + if sort_by_time then 41 + List.sort (fun (wpt1 : waypoint) (wpt2 : waypoint) -> 42 + match wpt1.time, wpt2.time with 43 + | Some t1, Some t2 -> Ptime.compare t1 t2 44 + | Some _, None -> -1 45 + | None, Some _ -> 1 46 + | None, None -> 0 47 + ) waypoints 48 + else if sort_by_name then 49 + List.sort (fun (wpt1 : waypoint) (wpt2 : waypoint) -> 50 + match wpt1.name, wpt2.name with 51 + | Some n1, Some n2 -> String.compare n1 n2 52 + | Some _, None -> -1 53 + | None, Some _ -> 1 54 + | None, None -> 0 55 + ) waypoints 56 + else 57 + waypoints 58 + 59 + (* Main conversion command *) 60 + let convert_waypoints_to_trackset input_file output_file track_name track_desc 61 + sort_by_time sort_by_name preserve_waypoints verbose style_renderer = 62 + setup_fmt style_renderer; 63 + let run env = 64 + try 65 + let fs = Eio.Stdenv.fs env in 66 + 67 + if verbose then 68 + log_info "Reading GPX file: %a" (bold_style Fmt.string) input_file; 69 + 70 + (* Read input GPX *) 71 + let gpx = Gpx_eio.read ~fs input_file in 72 + 73 + if verbose then 74 + log_info "Found %d waypoints and %d existing tracks" 75 + (List.length gpx.waypoints) 76 + (List.length gpx.tracks); 77 + 78 + (* Check if we have waypoints to convert *) 79 + if gpx.waypoints = [] then ( 80 + log_error "Input file contains no waypoints - nothing to convert"; 81 + exit 1 82 + ); 83 + 84 + (* Sort waypoints if requested *) 85 + let sorted_waypoints = sort_waypoints sort_by_time sort_by_name gpx.waypoints in 86 + 87 + if verbose && (sort_by_time || sort_by_name) then 88 + log_info "Sorted %d waypoints" (List.length sorted_waypoints); 89 + 90 + (* Convert waypoints to track segments *) 91 + let track_segments = waypoints_to_track_segments sorted_waypoints in 92 + 93 + (* Create the new track *) 94 + let new_track = { 95 + name = Some track_name; 96 + cmt = Some "Generated from waypoints by mlgpx CLI"; 97 + desc = track_desc; 98 + src = Some "mlgpx"; 99 + links = []; 100 + number = None; 101 + type_ = Some "converted"; 102 + extensions = []; 103 + trksegs = track_segments; 104 + } in 105 + 106 + if verbose then ( 107 + let total_points = List.fold_left (fun acc seg -> acc + List.length seg.trkpts) 0 track_segments in 108 + log_info "Created track %a with %d segments containing %d points" 109 + (bold_style Fmt.string) track_name 110 + (List.length track_segments) total_points 111 + ); 112 + 113 + (* Build output GPX *) 114 + let output_gpx = { 115 + gpx with 116 + waypoints = (if preserve_waypoints then gpx.waypoints else []); 117 + tracks = new_track :: gpx.tracks; 118 + metadata = (match gpx.metadata with 119 + | Some meta -> Some { meta with 120 + desc = Some (match meta.desc with 121 + | Some existing -> existing ^ " (waypoints converted to track)" 122 + | None -> "Waypoints converted to track") } 123 + | None -> Some { empty_metadata with 124 + desc = Some "Waypoints converted to track"; 125 + time = None }) 126 + } in 127 + 128 + (* Validate output *) 129 + let validation = validate_gpx output_gpx in 130 + if not validation.is_valid then ( 131 + log_error "Generated GPX failed validation:"; 132 + List.iter (fun issue -> 133 + let level_str = match issue.level with `Error -> "ERROR" | `Warning -> "WARNING" in 134 + let level_color = match issue.level with `Error -> error_style | `Warning -> warn_style in 135 + Fmt.pf Format.err_formatter " %a: %s\n" (level_color Fmt.string) level_str issue.message 136 + ) validation.issues; 137 + exit 1 138 + ); 139 + 140 + if verbose then 141 + log_info "Writing output to: %a" (bold_style Fmt.string) output_file; 142 + 143 + (* Write output GPX *) 144 + Gpx_eio.write ~fs output_file output_gpx; 145 + 146 + if verbose then ( 147 + Fmt.pf Format.std_formatter "%a\n" (success_style Fmt.string) "Conversion completed successfully!"; 148 + log_info "Output contains:"; 149 + Fmt.pf Format.err_formatter " - %d waypoints%s\n" 150 + (List.length output_gpx.waypoints) 151 + (if preserve_waypoints then " (preserved)" else " (removed)"); 152 + Fmt.pf Format.err_formatter " - %d tracks (%a + %d existing)\n" 153 + (List.length output_gpx.tracks) 154 + (success_style Fmt.string) "1 new" 155 + (List.length gpx.tracks) 156 + ) else ( 157 + log_success "Converted %d waypoints to track: %a → %a" 158 + (List.length sorted_waypoints) 159 + (bold_style Fmt.string) input_file 160 + (bold_style Fmt.string) output_file 161 + ) 162 + 163 + with 164 + | Gpx.Gpx_error err -> 165 + log_error "GPX Error: %s" (match err with 166 + | Invalid_xml s -> "Invalid XML: " ^ s 167 + | Invalid_coordinate s -> "Invalid coordinate: " ^ s 168 + | Missing_required_attribute (elem, attr) -> 169 + Printf.sprintf "Missing attribute %s in %s" attr elem 170 + | Missing_required_element s -> "Missing element: " ^ s 171 + | Validation_error s -> "Validation error: " ^ s 172 + | Xml_error s -> "XML error: " ^ s 173 + | IO_error s -> "I/O error: " ^ s); 174 + exit 2 175 + | Sys_error msg -> 176 + log_error "System error: %s" msg; 177 + exit 2 178 + | exn -> 179 + log_error "Unexpected error: %s" (Printexc.to_string exn); 180 + exit 2 181 + in 182 + Eio_main.run run 183 + 184 + (* Helper function to collect all timestamps from GPX *) 185 + let collect_all_timestamps gpx = 186 + let times = ref [] in 187 + 188 + (* Collect from waypoints *) 189 + List.iter (fun (wpt : waypoint) -> 190 + match wpt.time with 191 + | Some t -> times := t :: !times 192 + | None -> () 193 + ) gpx.waypoints; 194 + 195 + (* Collect from routes *) 196 + List.iter (fun route -> 197 + List.iter (fun (rtept : route_point) -> 198 + match rtept.time with 199 + | Some t -> times := t :: !times 200 + | None -> () 201 + ) route.rtepts 202 + ) gpx.routes; 203 + 204 + (* Collect from tracks *) 205 + List.iter (fun track -> 206 + List.iter (fun seg -> 207 + List.iter (fun (trkpt : track_point) -> 208 + match trkpt.time with 209 + | Some t -> times := t :: !times 210 + | None -> () 211 + ) seg.trkpts 212 + ) track.trksegs 213 + ) gpx.tracks; 214 + 215 + !times 216 + 217 + (* Info command *) 218 + let info_command input_file verbose style_renderer = 219 + setup_fmt style_renderer; 220 + let run env = 221 + try 222 + let fs = Eio.Stdenv.fs env in 223 + 224 + if verbose then 225 + log_info "Analyzing GPX file: %a" (bold_style Fmt.string) input_file; 226 + 227 + let gpx = Gpx_eio.read ~fs input_file in 228 + 229 + (* Header *) 230 + Fmt.pf Format.std_formatter "%a\n" (bold_style Fmt.string) "GPX File Information"; 231 + 232 + (* Basic info *) 233 + Printf.printf " Version: %s\n" gpx.version; 234 + Printf.printf " Creator: %s\n" gpx.creator; 235 + 236 + (match gpx.metadata with 237 + | Some meta -> 238 + Printf.printf " Name: %s\n" (Option.value meta.name ~default:"<unnamed>"); 239 + Printf.printf " Description: %s\n" (Option.value meta.desc ~default:"<none>"); 240 + (match meta.time with 241 + | Some time -> Printf.printf " Created: %s\n" (Ptime.to_rfc3339 time) 242 + | None -> ()) 243 + | None -> 244 + Printf.printf " No metadata\n"); 245 + 246 + (* Content summary *) 247 + Fmt.pf Format.std_formatter "\n%a\n" (bold_style Fmt.string) "Content Summary"; 248 + Printf.printf " Waypoints: %d\n" (List.length gpx.waypoints); 249 + Printf.printf " Routes: %d\n" (List.length gpx.routes); 250 + Printf.printf " Tracks: %d\n" (List.length gpx.tracks); 251 + 252 + (* Time range *) 253 + let all_times = collect_all_timestamps gpx in 254 + if all_times <> [] then ( 255 + let sorted_times = List.sort Ptime.compare all_times in 256 + let start_time = List.hd sorted_times in 257 + let stop_time = List.hd (List.rev sorted_times) in 258 + 259 + Fmt.pf Format.std_formatter "\n%a\n" (bold_style Fmt.string) "Time Range"; 260 + Fmt.pf Format.std_formatter " Start: %a\n" (info_style Fmt.string) (Ptime.to_rfc3339 start_time); 261 + Fmt.pf Format.std_formatter " Stop: %a\n" (info_style Fmt.string) (Ptime.to_rfc3339 stop_time); 262 + 263 + (* Calculate duration *) 264 + let duration_span = Ptime.diff stop_time start_time in 265 + match Ptime.Span.to_int_s duration_span with 266 + | Some seconds -> 267 + let days = seconds / 86400 in 268 + let hours = (seconds mod 86400) / 3600 in 269 + let minutes = (seconds mod 3600) / 60 in 270 + 271 + if days > 0 then 272 + Fmt.pf Format.std_formatter " Duration: %a\n" (bold_style Fmt.string) 273 + (Printf.sprintf "%d days, %d hours, %d minutes" days hours minutes) 274 + else if hours > 0 then 275 + Fmt.pf Format.std_formatter " Duration: %a\n" (bold_style Fmt.string) 276 + (Printf.sprintf "%d hours, %d minutes" hours minutes) 277 + else 278 + Fmt.pf Format.std_formatter " Duration: %a\n" (bold_style Fmt.string) 279 + (Printf.sprintf "%d minutes" minutes) 280 + | None -> 281 + (* Duration too large to represent as int *) 282 + Fmt.pf Format.std_formatter " Duration: %a\n" (bold_style Fmt.string) 283 + (Printf.sprintf "%.1f days" (Ptime.Span.to_float_s duration_span /. 86400.)); 284 + 285 + Printf.printf " Total points with timestamps: %d\n" (List.length all_times) 286 + ); 287 + 288 + (* Detailed waypoint info *) 289 + if gpx.waypoints <> [] then ( 290 + Fmt.pf Format.std_formatter "\n%a\n" (bold_style Fmt.string) "Waypoints"; 291 + let waypoints_with_time = List.filter (fun (wpt : waypoint) -> wpt.time <> None) gpx.waypoints in 292 + let waypoints_with_elevation = List.filter (fun (wpt : waypoint) -> wpt.ele <> None) gpx.waypoints in 293 + Printf.printf " - %d with timestamps\n" (List.length waypoints_with_time); 294 + Printf.printf " - %d with elevation data\n" (List.length waypoints_with_elevation); 295 + 296 + if verbose && List.length gpx.waypoints <= 10 then ( 297 + Printf.printf " Details:\n"; 298 + List.iteri (fun i (wpt : waypoint) -> 299 + Fmt.pf Format.std_formatter " %a %s (%.6f, %.6f)%s%s\n" 300 + (info_style Fmt.string) (Printf.sprintf "%d." (i + 1)) 301 + (Option.value wpt.name ~default:"<unnamed>") 302 + (latitude_to_float wpt.lat) (longitude_to_float wpt.lon) 303 + (match wpt.ele with Some e -> Printf.sprintf " elev=%.1fm" e | None -> "") 304 + (match wpt.time with Some t -> " @" ^ Ptime.to_rfc3339 t | None -> "") 305 + ) gpx.waypoints 306 + ) 307 + ); 308 + 309 + (* Track info *) 310 + if gpx.tracks <> [] then ( 311 + Fmt.pf Format.std_formatter "\n%a\n" (bold_style Fmt.string) "Tracks"; 312 + List.iteri (fun i track -> 313 + let total_points = List.fold_left (fun acc seg -> acc + List.length seg.trkpts) 0 track.trksegs in 314 + Fmt.pf Format.std_formatter " %a %s (%d segments, %d points)\n" 315 + (info_style Fmt.string) (Printf.sprintf "%d." (i + 1)) 316 + (Option.value track.name ~default:"<unnamed>") 317 + (List.length track.trksegs) total_points 318 + ) gpx.tracks 319 + ); 320 + 321 + (* Validation *) 322 + let validation = validate_gpx gpx in 323 + Printf.printf "\n"; 324 + if validation.is_valid then 325 + Fmt.pf Format.std_formatter "Validation: %a\n" (success_style Fmt.string) "PASSED" 326 + else ( 327 + Fmt.pf Format.std_formatter "Validation: %a\n" (error_style Fmt.string) "FAILED"; 328 + List.iter (fun issue -> 329 + let level_str = match issue.level with `Error -> "ERROR" | `Warning -> "WARNING" in 330 + let level_color = match issue.level with `Error -> error_style | `Warning -> warn_style in 331 + Fmt.pf Format.std_formatter " %a: %s\n" (level_color Fmt.string) level_str issue.message 332 + ) validation.issues 333 + ) 334 + 335 + with 336 + | Gpx.Gpx_error err -> 337 + log_error "GPX Error: %s" (match err with 338 + | Invalid_xml s -> "Invalid XML: " ^ s 339 + | Invalid_coordinate s -> "Invalid coordinate: " ^ s 340 + | Missing_required_attribute (elem, attr) -> 341 + Printf.sprintf "Missing attribute %s in %s" attr elem 342 + | Missing_required_element s -> "Missing element: " ^ s 343 + | Validation_error s -> "Validation error: " ^ s 344 + | Xml_error s -> "XML error: " ^ s 345 + | IO_error s -> "I/O error: " ^ s); 346 + exit 2 347 + | Sys_error msg -> 348 + log_error "System error: %s" msg; 349 + exit 2 350 + | exn -> 351 + log_error "Unexpected error: %s" (Printexc.to_string exn); 352 + exit 2 353 + in 354 + Eio_main.run run 355 + 356 + (* CLI argument definitions *) 357 + let input_file_arg = 358 + let doc = "Input GPX file path" in 359 + Arg.(required & pos 0 (some non_dir_file) None & info [] ~docv:"INPUT" ~doc) 360 + 361 + let output_file_arg = 362 + let doc = "Output GPX file path" in 363 + Arg.(required & pos 1 (some string) None & info [] ~docv:"OUTPUT" ~doc) 364 + 365 + let track_name_opt = 366 + let doc = "Name for the generated track (default: \"Converted from waypoints\")" in 367 + Arg.(value & opt string "Converted from waypoints" & info ["n"; "name"] ~docv:"NAME" ~doc) 368 + 369 + let track_description_opt = 370 + let doc = "Description for the generated track" in 371 + Arg.(value & opt (some string) None & info ["d"; "desc"] ~docv:"DESC" ~doc) 372 + 373 + let sort_by_time_flag = 374 + let doc = "Sort waypoints by timestamp before conversion" in 375 + Arg.(value & flag & info ["t"; "sort-time"] ~doc) 376 + 377 + let sort_by_name_flag = 378 + let doc = "Sort waypoints by name before conversion" in 379 + Arg.(value & flag & info ["sort-name"] ~doc) 380 + 381 + let preserve_waypoints_flag = 382 + let doc = "Keep original waypoints in addition to generated track" in 383 + Arg.(value & flag & info ["p"; "preserve"] ~doc) 384 + 385 + let verbose_flag = 386 + let doc = "Enable verbose output" in 387 + Arg.(value & flag & info ["v"; "verbose"] ~doc) 388 + 389 + (* Command definitions *) 390 + let convert_cmd = 391 + let doc = "Convert waypoints to trackset" in 392 + let man = [ 393 + `S Manpage.s_description; 394 + `P "Convert all waypoints in a GPX file to a single track. This is useful for \ 395 + converting a collection of waypoints into a navigable route or for \ 396 + consolidating GPS data."; 397 + `P "The conversion preserves all waypoint data (coordinates, elevation, \ 398 + timestamps, etc.) in the track points. By default, waypoints are removed \ 399 + from the output file unless --preserve is used."; 400 + `S Manpage.s_examples; 401 + `P "Convert waypoints to track:"; 402 + `Pre " mlgpx convert waypoints.gpx track.gpx"; 403 + `P "Convert with custom track name and preserve original waypoints:"; 404 + `Pre " mlgpx convert -n \"My Route\" --preserve waypoints.gpx route.gpx"; 405 + `P "Sort waypoints by timestamp before conversion:"; 406 + `Pre " mlgpx convert --sort-time waypoints.gpx sorted_track.gpx"; 407 + ] in 408 + let term = Term.(const convert_waypoints_to_trackset $ input_file_arg $ output_file_arg 409 + $ track_name_opt $ track_description_opt $ sort_by_time_flag 410 + $ sort_by_name_flag $ preserve_waypoints_flag $ verbose_flag 411 + $ Fmt_cli.style_renderer ()) in 412 + Cmd.v (Cmd.info "convert" ~doc ~man) term 413 + 414 + let info_cmd = 415 + let doc = "Display information about a GPX file" in 416 + let man = [ 417 + `S Manpage.s_description; 418 + `P "Analyze and display detailed information about a GPX file including \ 419 + statistics, content summary, and validation results."; 420 + `P "This command is useful for understanding the structure and content \ 421 + of GPX files before processing them."; 422 + `S Manpage.s_examples; 423 + `P "Show basic information:"; 424 + `Pre " mlgpx info file.gpx"; 425 + `P "Show detailed information with waypoint details:"; 426 + `Pre " mlgpx info -v file.gpx"; 427 + ] in 428 + let input_arg = 429 + let doc = "GPX file to analyze" in 430 + Arg.(required & pos 0 (some non_dir_file) None & info [] ~docv:"FILE" ~doc) in 431 + let term = Term.(const info_command $ input_arg $ verbose_flag 432 + $ Fmt_cli.style_renderer ()) in 433 + Cmd.v (Cmd.info "info" ~doc ~man) term 434 + 435 + (* Main CLI *) 436 + let main_cmd = 437 + let doc = "mlgpx - GPX file manipulation toolkit" in 438 + let man = [ 439 + `S Manpage.s_description; 440 + `P "mlgpx is a command-line toolkit for working with GPX (GPS Exchange Format) \ 441 + files. It provides tools for converting, analyzing, and manipulating GPS data."; 442 + `S Manpage.s_commands; 443 + `P "Available commands:"; 444 + `P "$(b,convert) - Convert waypoints to trackset"; 445 + `P "$(b,info) - Display GPX file information"; 446 + `S Manpage.s_common_options; 447 + `P "$(b,--verbose), $(b,-v) - Enable verbose output"; 448 + `P "$(b,--color)={auto|always|never} - Control ANSI color output"; 449 + `P "$(b,--help) - Show command help"; 450 + `S Manpage.s_examples; 451 + `P "Convert waypoints to track:"; 452 + `Pre " mlgpx convert waypoints.gpx track.gpx"; 453 + `P "Analyze a GPX file with colors:"; 454 + `Pre " mlgpx info --verbose --color=always file.gpx"; 455 + `P "Convert without colors for scripts:"; 456 + `Pre " mlgpx convert --color=never waypoints.gpx track.gpx"; 457 + `S Manpage.s_bugs; 458 + `P "Report bugs at https://github.com/avsm/mlgpx/issues"; 459 + ] in 460 + let default_term = Term.(ret (const (`Help (`Pager, None)))) in 461 + Cmd.group (Cmd.info "mlgpx" ~version:"0.1.0" ~doc ~man) ~default:default_term 462 + [convert_cmd; info_cmd] 463 + 464 + let () = 465 + Printexc.record_backtrace true; 466 + exit (Cmd.eval main_cmd)
+1 -1
dune-project
··· 2 2 3 3 (package 4 4 (name mlgpx) 5 - (depends ocaml dune xmlm ptime eio ppx_expect alcotest eio_main) 5 + (depends ocaml dune xmlm ptime eio ppx_expect alcotest eio_main cmdliner fmt logs) 6 6 (synopsis "OCaml library for parsing and generating GPX files") 7 7 (description 8 8 "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
··· 3 3 open Gpx 4 4 5 5 let () = 6 - Printf.printf "=== MLGpx Library Example ===\n\n"; 6 + Printf.printf "=== mlgpx Library Example ===\n\n"; 7 7 8 8 (* Create coordinates using direct API *) 9 9 let create_coordinate_pair lat_f lon_f =
+1 -1
lib/gpx/gpx.ml
··· 1 - (** {1 MLGpx - OCaml GPX Library} *) 1 + (** {1 mlgpx - OCaml GPX Library} *) 2 2 3 3 (** Core type definitions and utilities *) 4 4 module Types = Types
+2 -2
lib/gpx/parser.ml
··· 271 271 parser.current_element <- ["gpx"]; 272 272 let* version = require_attribute "version" attrs "gpx" in 273 273 let* creator = require_attribute "creator" attrs "gpx" in 274 - if version <> "1.1" then 275 - Error (Validation_error ("Unsupported GPX version: " ^ version)) 274 + if version <> "1.0" && version <> "1.1" then 275 + Error (Validation_error ("Unsupported GPX version: " ^ version ^ " (supported: 1.0, 1.1)")) 276 276 else 277 277 Ok (version, creator) 278 278 | `El_start _ ->
+1 -1
lib/gpx/types.ml
··· 156 156 157 157 (** Main GPX document *) 158 158 type gpx = { 159 - version : string; (* Always "1.1" for this version *) 159 + version : string; (* GPX version: "1.0" or "1.1" *) 160 160 creator : string; 161 161 metadata : metadata option; 162 162 waypoints : waypoint list;
+5 -2
lib/gpx/validate.ml
··· 167 167 let issues = ref [] in 168 168 169 169 (* Check GPX version *) 170 - if gpx.version <> "1.1" then 170 + if gpx.version <> "1.0" && gpx.version <> "1.1" then 171 171 issues := make_error ~location:"gpx" 172 - (Printf.sprintf "Unsupported GPX version: %s" gpx.version) :: !issues; 172 + (Printf.sprintf "Unsupported GPX version: %s (supported: 1.0, 1.1)" gpx.version) :: !issues 173 + else if gpx.version = "1.0" then 174 + issues := make_warning ~location:"gpx" 175 + "GPX 1.0 detected - consider upgrading to GPX 1.1 for better compatibility" :: !issues; 173 176 174 177 (* Check for empty creator *) 175 178 if String.trim gpx.creator = "" then