1(** GPX validation utilities *)
2
3open Types
4
5(** Validation error messages *)
6type validation_issue = {
7 level : [`Error | `Warning];
8 message : string;
9 location : string option;
10}
11
12type validation_result = {
13 issues : validation_issue list;
14 is_valid : bool;
15}
16
17let make_error ?location message = {
18 level = `Error;
19 message;
20 location;
21}
22
23let make_warning ?location message = {
24 level = `Warning;
25 message;
26 location;
27}
28
29(** Validate waypoint data *)
30let validate_waypoint_data wpt location =
31 let issues = ref [] in
32
33 (* Check for negative satellite count *)
34 (match wpt.sat with
35 | Some sat when sat < 0 ->
36 issues := make_warning ~location ("Negative satellite count: " ^ string_of_int sat) :: !issues
37 | _ -> ());
38
39 (* Check for unreasonable precision values *)
40 let check_precision name value =
41 match value with
42 | Some v when v < 0.0 ->
43 issues := make_warning ~location (Printf.sprintf "Negative %s value: %.2f" name v) :: !issues
44 | Some v when v > 1000.0 ->
45 issues := make_warning ~location (Printf.sprintf "Very large %s value: %.2f" name v) :: !issues
46 | _ -> ()
47 in
48
49 check_precision "hdop" wpt.hdop;
50 check_precision "vdop" wpt.vdop;
51 check_precision "pdop" wpt.pdop;
52
53 (* Check elevation reasonableness *)
54 (match wpt.ele with
55 | Some ele when ele < -15000.0 ->
56 issues := make_warning ~location (Printf.sprintf "Very low elevation: %.2f m" ele) :: !issues
57 | Some ele when ele > 15000.0 ->
58 issues := make_warning ~location (Printf.sprintf "Very high elevation: %.2f m" ele) :: !issues
59 | _ -> ());
60
61 (* Check DGPS age *)
62 (match wpt.ageofdgpsdata with
63 | Some age when age < 0.0 ->
64 issues := make_error ~location "Negative DGPS age" :: !issues
65 | _ -> ());
66
67 !issues
68
69(** Validate bounds *)
70let validate_bounds bounds =
71 let issues = ref [] in
72 let location = "bounds" in
73
74 if latitude_to_float bounds.minlat >= latitude_to_float bounds.maxlat then
75 issues := make_error ~location "minlat must be less than maxlat" :: !issues;
76
77 if longitude_to_float bounds.minlon >= longitude_to_float bounds.maxlon then
78 issues := make_error ~location "minlon must be less than maxlon" :: !issues;
79
80 !issues
81
82(** Validate metadata *)
83let validate_metadata metadata =
84 let issues = ref [] in
85
86 (* Validate bounds if present *)
87 (match metadata.bounds with
88 | Some bounds -> issues := validate_bounds bounds @ !issues
89 | None -> ());
90
91 (* Check for reasonable copyright year *)
92 (match metadata.copyright with
93 | Some copyright ->
94 (match copyright.year with
95 | Some year when year < 1900 || year > 2100 ->
96 issues := make_warning ~location:"metadata.copyright"
97 (Printf.sprintf "Unusual copyright year: %d" year) :: !issues
98 | _ -> ())
99 | None -> ());
100
101 !issues
102
103(** Validate route *)
104let validate_route route =
105 let issues = ref [] in
106 let location = "route" in
107
108 (* Check for empty route *)
109 if route.rtepts = [] then
110 issues := make_warning ~location "Route has no points" :: !issues;
111
112 (* Validate route points *)
113 List.iteri (fun i rtept ->
114 let point_location = Printf.sprintf "route.rtept[%d]" i in
115 issues := validate_waypoint_data rtept point_location @ !issues
116 ) route.rtepts;
117
118 !issues
119
120(** Validate track segment *)
121let validate_track_segment trkseg seg_idx =
122 let issues = ref [] in
123 let location = Printf.sprintf "track.trkseg[%d]" seg_idx in
124
125 (* Check for empty segment *)
126 if trkseg.trkpts = [] then
127 issues := make_warning ~location "Track segment has no points" :: !issues;
128
129 (* Validate track points *)
130 List.iteri (fun i trkpt ->
131 let point_location = Printf.sprintf "%s.trkpt[%d]" location i in
132 issues := validate_waypoint_data trkpt point_location @ !issues
133 ) trkseg.trkpts;
134
135 (* Check for time ordering if timestamps are present *)
136 let rec check_time_order prev_time = function
137 | [] -> ()
138 | trkpt :: rest ->
139 (match (prev_time, trkpt.time) with
140 | (Some prev, Some curr) when Ptime.compare prev curr > 0 ->
141 issues := make_warning ~location "Track points not in chronological order" :: !issues
142 | _ -> ());
143 check_time_order trkpt.time rest
144 in
145 check_time_order None trkseg.trkpts;
146
147 !issues
148
149(** Validate track *)
150let validate_track track =
151 let issues = ref [] in
152 let location = "track" in
153
154 (* Check for empty track *)
155 if track.trksegs = [] then
156 issues := make_warning ~location "Track has no segments" :: !issues;
157
158 (* Validate track segments *)
159 List.iteri (fun i trkseg ->
160 issues := validate_track_segment trkseg i @ !issues
161 ) track.trksegs;
162
163 !issues
164
165(** Validate complete GPX document *)
166let validate_gpx gpx =
167 let issues = ref [] in
168
169 (* Check GPX version *)
170 if gpx.version <> "1.0" && gpx.version <> "1.1" then
171 issues := make_error ~location:"gpx"
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;
176
177 (* Check for empty creator *)
178 if String.trim gpx.creator = "" then
179 issues := make_error ~location:"gpx" "Creator cannot be empty" :: !issues;
180
181 (* Validate metadata *)
182 (match gpx.metadata with
183 | Some metadata -> issues := validate_metadata metadata @ !issues
184 | None -> ());
185
186 (* Validate waypoints *)
187 List.iteri (fun i wpt ->
188 let location = Printf.sprintf "waypoint[%d]" i in
189 issues := validate_waypoint_data wpt location @ !issues
190 ) gpx.waypoints;
191
192 (* Validate routes *)
193 List.iteri (fun _i route ->
194 issues := validate_route route @ !issues
195 ) gpx.routes;
196
197 (* Validate tracks *)
198 List.iteri (fun _i track ->
199 issues := validate_track track @ !issues
200 ) gpx.tracks;
201
202 (* Check for completely empty GPX *)
203 if gpx.waypoints = [] && gpx.routes = [] && gpx.tracks = [] then
204 issues := make_warning ~location:"gpx" "GPX document contains no geographic data" :: !issues;
205
206 let all_issues = !issues in
207 let has_errors = List.exists (fun issue -> issue.level = `Error) all_issues in
208
209 { issues = all_issues; is_valid = not has_errors }
210
211(** Quick validation - returns true if document is valid *)
212let is_valid gpx =
213 let result = validate_gpx gpx in
214 result.is_valid
215
216(** Get only error messages *)
217let get_errors gpx =
218 let result = validate_gpx gpx in
219 List.filter (fun issue -> issue.level = `Error) result.issues
220
221(** Get only warning messages *)
222let get_warnings gpx =
223 let result = validate_gpx gpx in
224 List.filter (fun issue -> issue.level = `Warning) result.issues
225
226(** Format validation issue for display *)
227let format_issue issue =
228 let level_str = match issue.level with
229 | `Error -> "ERROR"
230 | `Warning -> "WARNING"
231 in
232 let location_str = match issue.location with
233 | Some loc -> " at " ^ loc
234 | None -> ""
235 in
236 Printf.sprintf "%s%s: %s" level_str location_str issue.message