(** Bounding box module *) type t = { min_lat : float; min_lon : float; max_lat : float; max_lon : float; } let create ~min_lat ~min_lon ~max_lat ~max_lon = if not (Lat.is_valid min_lat) then raise (Error.Coordinate_error (Error.Invalid_latitude min_lat)); if not (Lat.is_valid max_lat) then raise (Error.Coordinate_error (Error.Invalid_latitude max_lat)); if min_lat > max_lat then raise (Error.Coordinate_error (Error.Parse_error "min_lat must be less than or equal to max_lat")); { min_lat; min_lon = Lon.normalize_180 min_lon; max_lat; max_lon = Lon.normalize_180 max_lon; } let of_coords sw ne = create ~min_lat:(Coord.lat_float sw) ~min_lon:(Coord.lon_float sw) ~max_lat:(Coord.lat_float ne) ~max_lon:(Coord.lon_float ne) let of_list coords = match coords with | [] -> raise (Error.Coordinate_error (Error.Parse_error "Cannot create bbox from empty list")) | first :: rest -> let init_lat = Coord.lat_float first in let init_lon = Coord.lon_float first in let min_lat, min_lon, max_lat, max_lon = List.fold_left (fun (min_lat, min_lon, max_lat, max_lon) coord -> let lat = Coord.lat_float coord in let lon = Coord.lon_float coord in (min min_lat lat, min min_lon lon, max max_lat lat, max max_lon lon) ) (init_lat, init_lon, init_lat, init_lon) rest in create ~min_lat ~min_lon ~max_lat ~max_lon let min_lat t = t.min_lat let min_lon t = t.min_lon let max_lat t = t.max_lat let max_lon t = t.max_lon let sw_corner t = Coord.create ~lat:t.min_lat ~lon:t.min_lon let se_corner t = Coord.create ~lat:t.min_lat ~lon:t.max_lon let ne_corner t = Coord.create ~lat:t.max_lat ~lon:t.max_lon let nw_corner t = Coord.create ~lat:t.max_lat ~lon:t.min_lon let center t = Coord.create ~lat:((t.min_lat +. t.max_lat) /. 2.0) ~lon:((t.min_lon +. t.max_lon) /. 2.0) let width t = if t.max_lon >= t.min_lon then t.max_lon -. t.min_lon else (* Handles wraparound case *) (t.max_lon +. 360.0) -. t.min_lon let height t = t.max_lat -. t.min_lat let contains t coord = let lat = Coord.lat_float coord in let lon = Coord.lon_float coord in lat >= t.min_lat && lat <= t.max_lat && if t.min_lon <= t.max_lon then (* Normal case *) lon >= t.min_lon && lon <= t.max_lon else (* Crosses antimeridian *) lon >= t.min_lon || lon <= t.max_lon let intersects t1 t2 = (* Check latitude overlap *) let lat_overlaps = not (t1.max_lat < t2.min_lat || t2.max_lat < t1.min_lat) in (* Check longitude overlap *) let lon_overlaps = if t1.min_lon <= t1.max_lon && t2.min_lon <= t2.max_lon then (* Neither crosses antimeridian *) not (t1.max_lon < t2.min_lon || t2.max_lon < t1.min_lon) else if t1.min_lon > t1.max_lon && t2.min_lon > t2.max_lon then (* Both cross antimeridian *) true (* They must overlap *) else if t1.min_lon > t1.max_lon then (* Only t1 crosses antimeridian *) t2.max_lon >= t1.min_lon || t2.min_lon <= t1.max_lon else (* Only t2 crosses antimeridian *) t1.max_lon >= t2.min_lon || t1.min_lon <= t2.max_lon in lat_overlaps && lon_overlaps let union t1 t2 = create ~min_lat:(min t1.min_lat t2.min_lat) ~min_lon:(min t1.min_lon t2.min_lon) ~max_lat:(max t1.max_lat t2.max_lat) ~max_lon:(max t1.max_lon t2.max_lon) let intersection t1 t2 = if not (intersects t1 t2) then None else Some (create ~min_lat:(max t1.min_lat t2.min_lat) ~min_lon:(max t1.min_lon t2.min_lon) ~max_lat:(min t1.max_lat t2.max_lat) ~max_lon:(min t1.max_lon t2.max_lon)) let expand t degrees = create ~min_lat:(Lat.to_float (Lat.clamp (t.min_lat -. degrees))) ~min_lon:(t.min_lon -. degrees) ~max_lat:(Lat.to_float (Lat.clamp (t.max_lat +. degrees))) ~max_lon:(t.max_lon +. degrees) let expand_km t km = (* Approximate degrees per km at the center latitude *) let center_lat = (t.min_lat +. t.max_lat) /. 2.0 in let deg_lat_per_km = 1.0 /. 111.0 in (* ~111 km per degree latitude *) let deg_lon_per_km = 1.0 /. (111.0 *. cos (center_lat *. Float.pi /. 180.0)) in create ~min_lat:(Lat.to_float (Lat.clamp (t.min_lat -. km *. deg_lat_per_km))) ~min_lon:(t.min_lon -. km *. deg_lon_per_km) ~max_lat:(Lat.to_float (Lat.clamp (t.max_lat +. km *. deg_lat_per_km))) ~max_lon:(t.max_lon +. km *. deg_lon_per_km) let to_string t = Printf.sprintf "%.6f,%.6f,%.6f,%.6f" t.min_lon t.min_lat t.max_lon t.max_lat let of_string s = match String.split_on_char ',' s with | [min_lon; min_lat; max_lon; max_lat] -> (try create ~min_lat:(float_of_string (String.trim min_lat)) ~min_lon:(float_of_string (String.trim min_lon)) ~max_lat:(float_of_string (String.trim max_lat)) ~max_lon:(float_of_string (String.trim max_lon)) with Failure _ -> raise (Error.Coordinate_error (Error.Parse_error "Invalid bbox format: expected numeric values"))) | _ -> raise (Error.Coordinate_error (Error.Parse_error "Invalid bbox format: expected min_lon,min_lat,max_lon,max_lat"))