My agentic slop goes here. Not intended for anyone else!
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"))