My agentic slop goes here. Not intended for anyone else!
at main 5.2 kB view raw
1(** Bounding box module *) 2 3type t = { 4 min_lat : float; 5 min_lon : float; 6 max_lat : float; 7 max_lon : float; 8} 9 10let create ~min_lat ~min_lon ~max_lat ~max_lon = 11 if not (Lat.is_valid min_lat) then 12 raise (Error.Coordinate_error (Error.Invalid_latitude min_lat)); 13 if not (Lat.is_valid max_lat) then 14 raise (Error.Coordinate_error (Error.Invalid_latitude max_lat)); 15 if min_lat > max_lat then 16 raise (Error.Coordinate_error 17 (Error.Parse_error "min_lat must be less than or equal to max_lat")); 18 { 19 min_lat; 20 min_lon = Lon.normalize_180 min_lon; 21 max_lat; 22 max_lon = Lon.normalize_180 max_lon; 23 } 24 25let of_coords sw ne = 26 create 27 ~min_lat:(Coord.lat_float sw) 28 ~min_lon:(Coord.lon_float sw) 29 ~max_lat:(Coord.lat_float ne) 30 ~max_lon:(Coord.lon_float ne) 31 32let of_list coords = 33 match coords with 34 | [] -> raise (Error.Coordinate_error 35 (Error.Parse_error "Cannot create bbox from empty list")) 36 | first :: rest -> 37 let init_lat = Coord.lat_float first in 38 let init_lon = Coord.lon_float first in 39 let min_lat, min_lon, max_lat, max_lon = 40 List.fold_left (fun (min_lat, min_lon, max_lat, max_lon) coord -> 41 let lat = Coord.lat_float coord in 42 let lon = Coord.lon_float coord in 43 (min min_lat lat, min min_lon lon, max max_lat lat, max max_lon lon) 44 ) (init_lat, init_lon, init_lat, init_lon) rest 45 in 46 create ~min_lat ~min_lon ~max_lat ~max_lon 47 48let min_lat t = t.min_lat 49let min_lon t = t.min_lon 50let max_lat t = t.max_lat 51let max_lon t = t.max_lon 52 53let sw_corner t = Coord.create ~lat:t.min_lat ~lon:t.min_lon 54let se_corner t = Coord.create ~lat:t.min_lat ~lon:t.max_lon 55let ne_corner t = Coord.create ~lat:t.max_lat ~lon:t.max_lon 56let nw_corner t = Coord.create ~lat:t.max_lat ~lon:t.min_lon 57 58let center t = 59 Coord.create 60 ~lat:((t.min_lat +. t.max_lat) /. 2.0) 61 ~lon:((t.min_lon +. t.max_lon) /. 2.0) 62 63let width t = 64 if t.max_lon >= t.min_lon then 65 t.max_lon -. t.min_lon 66 else 67 (* Handles wraparound case *) 68 (t.max_lon +. 360.0) -. t.min_lon 69 70let height t = t.max_lat -. t.min_lat 71 72let contains t coord = 73 let lat = Coord.lat_float coord in 74 let lon = Coord.lon_float coord in 75 lat >= t.min_lat && lat <= t.max_lat && 76 if t.min_lon <= t.max_lon then 77 (* Normal case *) 78 lon >= t.min_lon && lon <= t.max_lon 79 else 80 (* Crosses antimeridian *) 81 lon >= t.min_lon || lon <= t.max_lon 82 83let intersects t1 t2 = 84 (* Check latitude overlap *) 85 let lat_overlaps = 86 not (t1.max_lat < t2.min_lat || t2.max_lat < t1.min_lat) in 87 88 (* Check longitude overlap *) 89 let lon_overlaps = 90 if t1.min_lon <= t1.max_lon && t2.min_lon <= t2.max_lon then 91 (* Neither crosses antimeridian *) 92 not (t1.max_lon < t2.min_lon || t2.max_lon < t1.min_lon) 93 else if t1.min_lon > t1.max_lon && t2.min_lon > t2.max_lon then 94 (* Both cross antimeridian *) 95 true (* They must overlap *) 96 else if t1.min_lon > t1.max_lon then 97 (* Only t1 crosses antimeridian *) 98 t2.max_lon >= t1.min_lon || t2.min_lon <= t1.max_lon 99 else 100 (* Only t2 crosses antimeridian *) 101 t1.max_lon >= t2.min_lon || t1.min_lon <= t2.max_lon 102 in 103 104 lat_overlaps && lon_overlaps 105 106let union t1 t2 = 107 create 108 ~min_lat:(min t1.min_lat t2.min_lat) 109 ~min_lon:(min t1.min_lon t2.min_lon) 110 ~max_lat:(max t1.max_lat t2.max_lat) 111 ~max_lon:(max t1.max_lon t2.max_lon) 112 113let intersection t1 t2 = 114 if not (intersects t1 t2) then 115 None 116 else 117 Some (create 118 ~min_lat:(max t1.min_lat t2.min_lat) 119 ~min_lon:(max t1.min_lon t2.min_lon) 120 ~max_lat:(min t1.max_lat t2.max_lat) 121 ~max_lon:(min t1.max_lon t2.max_lon)) 122 123let expand t degrees = 124 create 125 ~min_lat:(Lat.to_float (Lat.clamp (t.min_lat -. degrees))) 126 ~min_lon:(t.min_lon -. degrees) 127 ~max_lat:(Lat.to_float (Lat.clamp (t.max_lat +. degrees))) 128 ~max_lon:(t.max_lon +. degrees) 129 130let expand_km t km = 131 (* Approximate degrees per km at the center latitude *) 132 let center_lat = (t.min_lat +. t.max_lat) /. 2.0 in 133 let deg_lat_per_km = 1.0 /. 111.0 in (* ~111 km per degree latitude *) 134 let deg_lon_per_km = 1.0 /. (111.0 *. cos (center_lat *. Float.pi /. 180.0)) in 135 136 create 137 ~min_lat:(Lat.to_float (Lat.clamp (t.min_lat -. km *. deg_lat_per_km))) 138 ~min_lon:(t.min_lon -. km *. deg_lon_per_km) 139 ~max_lat:(Lat.to_float (Lat.clamp (t.max_lat +. km *. deg_lat_per_km))) 140 ~max_lon:(t.max_lon +. km *. deg_lon_per_km) 141 142let to_string t = 143 Printf.sprintf "%.6f,%.6f,%.6f,%.6f" t.min_lon t.min_lat t.max_lon t.max_lat 144 145let of_string s = 146 match String.split_on_char ',' s with 147 | [min_lon; min_lat; max_lon; max_lat] -> 148 (try 149 create 150 ~min_lat:(float_of_string (String.trim min_lat)) 151 ~min_lon:(float_of_string (String.trim min_lon)) 152 ~max_lat:(float_of_string (String.trim max_lat)) 153 ~max_lon:(float_of_string (String.trim max_lon)) 154 with Failure _ -> 155 raise (Error.Coordinate_error 156 (Error.Parse_error "Invalid bbox format: expected numeric values"))) 157 | _ -> 158 raise (Error.Coordinate_error 159 (Error.Parse_error "Invalid bbox format: expected min_lon,min_lat,max_lon,max_lat"))