GPS Exchange Format library/CLI in OCaml
1(** GPX XML writer with complete spec coverage *)
2
3(** Result binding operators *)
4let (let*) = Result.bind
5
6(** Helper to write XML elements *)
7let output_element_start writer name attrs =
8 try
9 Xmlm.output writer (`El_start ((("", name), attrs)));
10 Ok ()
11 with exn ->
12 Error (Error.xml_error (Printexc.to_string exn))
13
14let output_element_end writer =
15 try
16 Xmlm.output writer `El_end;
17 Ok ()
18 with exn ->
19 Error (Error.xml_error (Printexc.to_string exn))
20
21let output_data writer text =
22 try
23 Xmlm.output writer (`Data text);
24 Ok ()
25 with exn ->
26 Error (Error.xml_error (Printexc.to_string exn))
27
28let output_text_element writer name text =
29 let attrs = [] in
30 let* () = output_element_start writer name attrs in
31 let* () = output_data writer text in
32 output_element_end writer
33
34let output_optional_text_element writer name = function
35 | Some text -> output_text_element writer name text
36 | None -> Ok ()
37
38let output_optional_float_element writer name = function
39 | Some value -> output_text_element writer name (Printf.sprintf "%.6f" value)
40 | None -> Ok ()
41
42let output_optional_degrees_element writer name = function
43 | Some degrees -> output_text_element writer name (Printf.sprintf "%.6f" (Coordinate.degrees_to_float degrees))
44 | None -> Ok ()
45
46let output_optional_int_element writer name = function
47 | Some value -> output_text_element writer name (string_of_int value)
48 | None -> Ok ()
49
50let output_optional_time_element writer name = function
51 | Some time -> output_text_element writer name (Ptime.to_rfc3339 time)
52 | None -> Ok ()
53
54let output_optional_fix_element writer name = function
55 | Some fix_type -> output_text_element writer name (Waypoint.fix_type_to_string fix_type)
56 | None -> Ok ()
57
58(** Write link elements *)
59let output_link writer link =
60 let href = Link.href link in
61 let attrs = [(("", "href"), href)] in
62 let* () = output_element_start writer "link" attrs in
63 let* () = output_optional_text_element writer "text" (Link.text link) in
64 let* () = output_optional_text_element writer "type" (Link.type_ link) in
65 output_element_end writer
66
67let output_links writer links =
68 let rec write_links = function
69 | [] -> Ok ()
70 | link :: rest ->
71 let* () = output_link writer link in
72 write_links rest
73 in
74 write_links links
75
76(** Write person (author) element *)
77let output_person writer person =
78 let* () = output_element_start writer "author" [] in
79 let* () = output_optional_text_element writer "name" (Link.person_name person) in
80 let* () = match Link.person_email person with
81 | Some email ->
82 (* Parse email into id and domain *)
83 (match String.index_opt email '@' with
84 | Some at_pos ->
85 let id = String.sub email 0 at_pos in
86 let domain = String.sub email (at_pos + 1) (String.length email - at_pos - 1) in
87 let attrs = [(("", "id"), id); (("", "domain"), domain)] in
88 let* () = output_element_start writer "email" attrs in
89 output_element_end writer
90 | None ->
91 (* Invalid email format, skip *)
92 Ok ())
93 | None -> Ok ()
94 in
95 let* () = match Link.person_link person with
96 | Some link -> output_link writer link
97 | None -> Ok ()
98 in
99 output_element_end writer
100
101(** Write copyright element *)
102let output_copyright writer copyright =
103 let author = Link.copyright_author copyright in
104 let attrs = [(("", "author"), author)] in
105 let* () = output_element_start writer "copyright" attrs in
106 let* () = output_optional_int_element writer "year" (Link.copyright_year copyright) in
107 let* () = output_optional_text_element writer "license" (Link.copyright_license copyright) in
108 output_element_end writer
109
110(** Write bounds element *)
111let output_bounds writer bounds =
112 let (minlat, minlon, maxlat, maxlon) = Metadata.Bounds.bounds bounds in
113 let attrs = [
114 (("", "minlat"), Printf.sprintf "%.6f" (Coordinate.latitude_to_float minlat));
115 (("", "minlon"), Printf.sprintf "%.6f" (Coordinate.longitude_to_float minlon));
116 (("", "maxlat"), Printf.sprintf "%.6f" (Coordinate.latitude_to_float maxlat));
117 (("", "maxlon"), Printf.sprintf "%.6f" (Coordinate.longitude_to_float maxlon));
118 ] in
119 let* () = output_element_start writer "bounds" attrs in
120 output_element_end writer
121
122(** Write extensions element *)
123let output_extensions writer extensions =
124 if extensions = [] then Ok ()
125 else
126 let* () = output_element_start writer "extensions" [] in
127 (* For now, skip writing extension content - would need full extension serialization *)
128 output_element_end writer
129
130(** Write metadata element *)
131let output_metadata writer metadata =
132 let* () = output_element_start writer "metadata" [] in
133 let* () = output_optional_text_element writer "name" (Metadata.name metadata) in
134 let* () = output_optional_text_element writer "desc" (Metadata.description metadata) in
135 let* () = match Metadata.author metadata with
136 | Some author -> output_person writer author
137 | None -> Ok ()
138 in
139 let* () = match Metadata.copyright metadata with
140 | Some copyright -> output_copyright writer copyright
141 | None -> Ok ()
142 in
143 let* () = output_links writer (Metadata.links metadata) in
144 let* () = output_optional_time_element writer "time" (Metadata.time metadata) in
145 let* () = output_optional_text_element writer "keywords" (Metadata.keywords metadata) in
146 let* () = match Metadata.bounds_opt metadata with
147 | Some bounds -> output_bounds writer bounds
148 | None -> Ok ()
149 in
150 let* () = output_extensions writer (Metadata.extensions metadata) in
151 output_element_end writer
152
153(** Write waypoint elements (used for wpt, rtept, trkpt) *)
154let output_waypoint_data writer waypoint =
155 let* () = output_optional_float_element writer "ele" (Waypoint.elevation waypoint) in
156 let* () = output_optional_time_element writer "time" (Waypoint.time waypoint) in
157 let* () = output_optional_degrees_element writer "magvar" (Waypoint.magvar waypoint) in
158 let* () = output_optional_float_element writer "geoidheight" (Waypoint.geoidheight waypoint) in
159 let* () = output_optional_text_element writer "name" (Waypoint.name waypoint) in
160 let* () = output_optional_text_element writer "cmt" (Waypoint.comment waypoint) in
161 let* () = output_optional_text_element writer "desc" (Waypoint.description waypoint) in
162 let* () = output_optional_text_element writer "src" (Waypoint.source waypoint) in
163 let* () = output_links writer (Waypoint.links waypoint) in
164 let* () = output_optional_text_element writer "sym" (Waypoint.symbol waypoint) in
165 let* () = output_optional_text_element writer "type" (Waypoint.type_ waypoint) in
166 let* () = output_optional_fix_element writer "fix" (Waypoint.fix waypoint) in
167 let* () = output_optional_int_element writer "sat" (Waypoint.sat waypoint) in
168 let* () = output_optional_float_element writer "hdop" (Waypoint.hdop waypoint) in
169 let* () = output_optional_float_element writer "vdop" (Waypoint.vdop waypoint) in
170 let* () = output_optional_float_element writer "pdop" (Waypoint.pdop waypoint) in
171 let* () = output_optional_float_element writer "ageofdgpsdata" (Waypoint.ageofdgpsdata waypoint) in
172 let* () = output_optional_int_element writer "dgpsid" (Waypoint.dgpsid waypoint) in
173 output_extensions writer (Waypoint.extensions waypoint)
174
175(** Write waypoints *)
176let output_waypoints writer waypoints =
177 let rec write_waypoints = function
178 | [] -> Ok ()
179 | wpt :: rest ->
180 let lat = Coordinate.latitude_to_float (Waypoint.lat wpt) in
181 let lon = Coordinate.longitude_to_float (Waypoint.lon wpt) in
182 let attrs = [
183 (("", "lat"), Printf.sprintf "%.6f" lat);
184 (("", "lon"), Printf.sprintf "%.6f" lon);
185 ] in
186 let* () = output_element_start writer "wpt" attrs in
187 let* () = output_waypoint_data writer wpt in
188 let* () = output_element_end writer in
189 write_waypoints rest
190 in
191 write_waypoints waypoints
192
193(** Write route points *)
194let output_route_points writer points element_name =
195 let rec write_points = function
196 | [] -> Ok ()
197 | pt :: rest ->
198 let lat = Coordinate.latitude_to_float (Waypoint.lat pt) in
199 let lon = Coordinate.longitude_to_float (Waypoint.lon pt) in
200 let attrs = [
201 (("", "lat"), Printf.sprintf "%.6f" lat);
202 (("", "lon"), Printf.sprintf "%.6f" lon);
203 ] in
204 let* () = output_element_start writer element_name attrs in
205 let* () = output_waypoint_data writer pt in
206 let* () = output_element_end writer in
207 write_points rest
208 in
209 write_points points
210
211(** Write routes *)
212let output_routes writer routes =
213 let rec write_routes = function
214 | [] -> Ok ()
215 | route :: rest ->
216 let* () = output_element_start writer "rte" [] in
217 let* () = output_optional_text_element writer "name" (Route.name route) in
218 let* () = output_optional_text_element writer "cmt" (Route.comment route) in
219 let* () = output_optional_text_element writer "desc" (Route.description route) in
220 let* () = output_optional_text_element writer "src" (Route.source route) in
221 let* () = output_links writer (Route.links route) in
222 let* () = output_optional_int_element writer "number" (Route.number route) in
223 let* () = output_optional_text_element writer "type" (Route.type_ route) in
224 let* () = output_extensions writer (Route.extensions route) in
225 let* () = output_route_points writer (Route.points route) "rtept" in
226 let* () = output_element_end writer in
227 write_routes rest
228 in
229 write_routes routes
230
231(** Write track segments *)
232let output_track_segments writer segments =
233 let rec write_segments = function
234 | [] -> Ok ()
235 | seg :: rest ->
236 let* () = output_element_start writer "trkseg" [] in
237 let* () = output_route_points writer (Track.Segment.points seg) "trkpt" in
238 let* () = output_extensions writer (Track.Segment.extensions seg) in
239 let* () = output_element_end writer in
240 write_segments rest
241 in
242 write_segments segments
243
244(** Write tracks *)
245let output_tracks writer tracks =
246 let rec write_tracks = function
247 | [] -> Ok ()
248 | track :: rest ->
249 let* () = output_element_start writer "trk" [] in
250 let* () = output_optional_text_element writer "name" (Track.name track) in
251 let* () = output_optional_text_element writer "cmt" (Track.comment track) in
252 let* () = output_optional_text_element writer "desc" (Track.description track) in
253 let* () = output_optional_text_element writer "src" (Track.source track) in
254 let* () = output_links writer (Track.links track) in
255 let* () = output_optional_int_element writer "number" (Track.number track) in
256 let* () = output_optional_text_element writer "type" (Track.type_ track) in
257 let* () = output_extensions writer (Track.extensions track) in
258 let* () = output_track_segments writer (Track.segments track) in
259 let* () = output_element_end writer in
260 write_tracks rest
261 in
262 write_tracks tracks
263
264(** Write a complete GPX document *)
265let write ?(validate=false) output gpx =
266 let writer = Xmlm.make_output output in
267
268 let result =
269 try
270 (* Write XML declaration and GPX root element *)
271 let version = Doc.version gpx in
272 let creator = Doc.creator gpx in
273 let attrs = [
274 (("", "version"), version);
275 (("", "creator"), creator);
276 (("", "xsi:schemaLocation"), "http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd");
277 (("xmlns", "xsi"), "http://www.w3.org/2001/XMLSchema-instance");
278 (("", "xmlns"), "http://www.topografix.com/GPX/1/1")
279 ] in
280
281 let* () = output_element_start writer "gpx" attrs in
282
283 (* Write metadata if present *)
284 let* () = match Doc.metadata gpx with
285 | Some metadata -> output_metadata writer metadata
286 | None -> Ok ()
287 in
288
289 (* Write waypoints *)
290 let* () = output_waypoints writer (Doc.waypoints gpx) in
291
292 (* Write routes *)
293 let* () = output_routes writer (Doc.routes gpx) in
294
295 (* Write tracks *)
296 let* () = output_tracks writer (Doc.tracks gpx) in
297
298 (* Write root-level extensions *)
299 let* () = output_extensions writer (Doc.extensions gpx) in
300
301 output_element_end writer
302
303 with
304 | Xmlm.Error ((line, col), error) ->
305 Error (Error.xml_error (Printf.sprintf "XML error at line %d, column %d: %s"
306 line col (Xmlm.error_message error)))
307 | exn ->
308 Error (Error.xml_error (Printexc.to_string exn))
309 in
310
311 match result, validate with
312 | Ok (), true ->
313 let validation = Validate.validate_gpx gpx in
314 if validation.is_valid then
315 Ok ()
316 else
317 let error_msgs = List.filter (fun issue -> issue.Validate.level = `Error) validation.issues
318 |> List.map (fun issue -> issue.Validate.message)
319 |> String.concat "; " in
320 Error (Error.validation_error error_msgs)
321 | result, false -> result
322 | Error _ as result, true -> result (* Pass through write errors even when validating *)
323
324(** Write GPX to string *)
325let write_string ?(validate=false) gpx =
326 let buffer = Buffer.create 1024 in
327 let dest = `Buffer buffer in
328 let* () = write ~validate dest gpx in
329 Ok (Buffer.contents buffer)