(** Coordinate parsing module *) let parse_dms s = let s = String.trim s in (* Check for hemisphere markers *) let has_s = String.contains s 'S' || String.contains s 's' in let has_w = String.contains s 'W' || String.contains s 'w' in let sign = if has_s || has_w then -1.0 else 1.0 in (* Remove hemisphere markers and clean up *) let cleaned = s |> Str.global_replace (Str.regexp "[NnSsEeWw]") "" |> String.trim in (* Try to parse different formats *) try (* First try as simple decimal degrees *) if not (String.contains cleaned (Char.chr 176) || (* degree symbol *) String.contains cleaned '\'' || String.contains cleaned '"' || String.contains cleaned ' ' || String.contains cleaned ':') then sign *. float_of_string cleaned else (* Parse DMS format *) let deg_char = Char.chr 176 in (* degree symbol *) let parts = if String.contains cleaned deg_char then (* Format: DD degree MM min SS sec or DD degree MM.MMM min *) let deg_parts = String.split_on_char deg_char cleaned in match deg_parts with | [deg_str; rest] -> let deg = float_of_string (String.trim deg_str) in if String.contains rest '\'' then let min_parts = String.split_on_char '\'' rest in match min_parts with | [min_str; sec_str] -> let min = float_of_string (String.trim min_str) in let sec_str = String.trim sec_str in let sec_str = if String.contains sec_str '"' then String.sub sec_str 0 (String.index sec_str '"') else sec_str in let sec = if sec_str = "" then 0.0 else float_of_string sec_str in [deg; min; sec] | [min_str] -> let min = float_of_string (String.trim min_str) in [deg; min; 0.0] | _ -> raise (Error.Coordinate_error (Error.Parse_error "Invalid DMS format")) else [deg; 0.0; 0.0] | _ -> raise (Error.Coordinate_error (Error.Parse_error "Invalid degree format")) else if String.contains cleaned ':' then (* Format: DD:MM:SS *) cleaned |> String.split_on_char ':' |> List.map String.trim |> List.map float_of_string else if String.contains cleaned ' ' then (* Format: DD MM SS *) cleaned |> String.split_on_char ' ' |> List.filter (fun s -> String.length s > 0) |> List.map String.trim |> List.map float_of_string else raise (Error.Coordinate_error (Error.Parse_error "Unknown coordinate format")) in (* Convert to decimal degrees *) match parts with | [deg] -> sign *. deg | [deg; min] -> sign *. (abs_float deg +. min /. 60.0) | [deg; min; sec] -> sign *. (abs_float deg +. min /. 60.0 +. sec /. 3600.0) | _ -> raise (Error.Coordinate_error (Error.Parse_error "Invalid number of components")) with | Failure _ -> raise (Error.Coordinate_error (Error.Parse_error ("Failed to parse coordinate: " ^ s))) let parse_lat s = let value = parse_dms s in Lat.create value let parse_lon s = let value = parse_dms s in Lon.create value let parse_coord s = let s = String.trim s in (* Try comma-separated first *) let parts = if String.contains s ',' then String.split_on_char ',' s |> List.map String.trim else (* Try space-separated *) s |> String.split_on_char ' ' |> List.filter (fun s -> String.length s > 0) |> List.map String.trim in match parts with | [lat_str; lon_str] -> let lat = parse_dms lat_str in let lon = parse_dms lon_str in Coord.create ~lat ~lon | _ -> raise (Error.Coordinate_error (Error.Parse_error "Expected lat,lon or lat lon format")) let try_parse_lat s = try Some (parse_lat s) with Error.Coordinate_error _ -> None let try_parse_lon s = try Some (parse_lon s) with Error.Coordinate_error _ -> None let try_parse_coord s = try Some (parse_coord s) with Error.Coordinate_error _ -> None