My agentic slop goes here. Not intended for anyone else!

lonlat

+18
stack/lonlat/dune-project
···
+
(lang dune 3.0)
+
(name lonlat)
+
+
(generate_opam_files true)
+
+
(package
+
(name lonlat)
+
(synopsis "Geographic coordinate manipulation library for OCaml")
+
(description "LonLat provides types and operations for working with \
+
latitude/longitude coordinates, including distance calculations, \
+
bearings, bounding boxes, and coordinate parsing.")
+
(depends
+
ocaml
+
dune))
+
+
(authors "LonLat Authors")
+
(maintainers "maintainer@example.com")
+
(license MIT)
+159
stack/lonlat/lib/bbox.ml
···
+
(** 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"))
+53
stack/lonlat/lib/bbox.mli
···
+
(** Bounding box module *)
+
+
type t
+
+
(** {2 Construction} *)
+
+
val create : min_lat:float -> min_lon:float -> max_lat:float -> max_lon:float -> t
+
+
val of_coords : Coord.t -> Coord.t -> t
+
(** From SW and NE corners *)
+
+
val of_list : Coord.t list -> t
+
(** Compute bbox containing all points *)
+
+
(** {2 Accessors} *)
+
+
val min_lat : t -> float
+
val min_lon : t -> float
+
val max_lat : t -> float
+
val max_lon : t -> float
+
+
val sw_corner : t -> Coord.t
+
val se_corner : t -> Coord.t
+
val ne_corner : t -> Coord.t
+
val nw_corner : t -> Coord.t
+
val center : t -> Coord.t
+
+
val width : t -> float
+
(** Width in degrees *)
+
+
val height : t -> float
+
(** Height in degrees *)
+
+
(** {2 Operations} *)
+
+
val contains : t -> Coord.t -> bool
+
val intersects : t -> t -> bool
+
val union : t -> t -> t
+
val intersection : t -> t -> t option
+
+
val expand : t -> float -> t
+
(** Expand by degrees on all sides *)
+
+
val expand_km : t -> float -> t
+
(** Expand by approximate km on all sides *)
+
+
(** {2 Formatting} *)
+
+
val to_string : t -> string
+
(** Format as "min_lon,min_lat,max_lon,max_lat" *)
+
+
val of_string : string -> t
+
(** Parse from "min_lon,min_lat,max_lon,max_lat" *)
+195
stack/lonlat/lib/coord.ml
···
+
(** Coordinate pair module *)
+
+
type t = {
+
lat : Lat.t;
+
lon : Lon.t;
+
}
+
+
let create ~lat ~lon = {
+
lat = Lat.create lat;
+
lon = Lon.create lon;
+
}
+
+
let of_lat_lon lat lon = { lat; lon }
+
+
let lat t = t.lat
+
let lon t = t.lon
+
let lat_float t = Lat.to_float t.lat
+
let lon_float t = Lon.to_float t.lon
+
+
(** Constants *)
+
let earth_radius_km = 6371.0
+
let deg_to_rad = Float.pi /. 180.0
+
let rad_to_deg = 180.0 /. Float.pi
+
+
(** Distance calculations *)
+
+
let distance_haversine p1 p2 =
+
let lat1 = lat_float p1 *. deg_to_rad in
+
let lat2 = lat_float p2 *. deg_to_rad in
+
let lon1 = lon_float p1 *. deg_to_rad in
+
let lon2 = lon_float p2 *. deg_to_rad in
+
+
let dlat = lat2 -. lat1 in
+
let dlon = lon2 -. lon1 in
+
+
let a =
+
(sin (dlat /. 2.0) ** 2.0) +.
+
cos lat1 *. cos lat2 *. (sin (dlon /. 2.0) ** 2.0) in
+
+
let c = 2.0 *. atan2 (sqrt a) (sqrt (1.0 -. a)) in
+
earth_radius_km *. c
+
+
let distance_vincenty p1 p2 =
+
(* WGS84 ellipsoid constants *)
+
let a = 6378.137 in (* semi-major axis in km *)
+
let b = 6356.752 in (* semi-minor axis in km *)
+
let f = 1.0 /. 298.257223563 in (* flattening *)
+
+
let lat1 = lat_float p1 *. deg_to_rad in
+
let lat2 = lat_float p2 *. deg_to_rad in
+
let lon1 = lon_float p1 *. deg_to_rad in
+
let lon2 = lon_float p2 *. deg_to_rad in
+
+
let l = lon2 -. lon1 in
+
let u1 = atan ((1.0 -. f) *. tan lat1) in
+
let u2 = atan ((1.0 -. f) *. tan lat2) in
+
let sin_u1 = sin u1 in
+
let cos_u1 = cos u1 in
+
let sin_u2 = sin u2 in
+
let cos_u2 = cos u2 in
+
+
let rec iterate lambda _prev_lambda iter =
+
if iter > 100 then
+
(* Failed to converge, fall back to haversine *)
+
distance_haversine p1 p2
+
else
+
let sin_lambda = sin lambda in
+
let cos_lambda = cos lambda in
+
let sin_sigma =
+
sqrt ((cos_u2 *. sin_lambda) ** 2.0 +.
+
(cos_u1 *. sin_u2 -. sin_u1 *. cos_u2 *. cos_lambda) ** 2.0) in
+
+
if sin_sigma = 0.0 then 0.0 (* coincident points *)
+
else
+
let cos_sigma = sin_u1 *. sin_u2 +. cos_u1 *. cos_u2 *. cos_lambda in
+
let sigma = atan2 sin_sigma cos_sigma in
+
let sin_alpha = cos_u1 *. cos_u2 *. sin_lambda /. sin_sigma in
+
let cos_sq_alpha = 1.0 -. sin_alpha ** 2.0 in
+
let cos_2sigma_m =
+
if cos_sq_alpha = 0.0 then 0.0 (* equatorial line *)
+
else cos_sigma -. 2.0 *. sin_u1 *. sin_u2 /. cos_sq_alpha in
+
+
let c = f /. 16.0 *. cos_sq_alpha *. (4.0 +. f *. (4.0 -. 3.0 *. cos_sq_alpha)) in
+
let lambda' = l +. (1.0 -. c) *. f *. sin_alpha *.
+
(sigma +. c *. sin_sigma *.
+
(cos_2sigma_m +. c *. cos_sigma *.
+
(-1.0 +. 2.0 *. cos_2sigma_m ** 2.0))) in
+
+
if abs_float (lambda' -. lambda) < 1e-12 then
+
(* Converged *)
+
let u_sq = cos_sq_alpha *. (a ** 2.0 -. b ** 2.0) /. (b ** 2.0) in
+
let cap_a = 1.0 +. u_sq /. 16384.0 *.
+
(4096.0 +. u_sq *. (-768.0 +. u_sq *. (320.0 -. 175.0 *. u_sq))) in
+
let cap_b = u_sq /. 1024.0 *. (256.0 +. u_sq *. (-128.0 +. u_sq *. (74.0 -. 47.0 *. u_sq))) in
+
let delta_sigma = cap_b *. sin_sigma *.
+
(cos_2sigma_m +. cap_b /. 4.0 *.
+
(cos_sigma *. (-1.0 +. 2.0 *. cos_2sigma_m ** 2.0) -.
+
cap_b /. 6.0 *. cos_2sigma_m *.
+
(-3.0 +. 4.0 *. sin_sigma ** 2.0) *.
+
(-3.0 +. 4.0 *. cos_2sigma_m ** 2.0))) in
+
+
b *. cap_a *. (sigma -. delta_sigma)
+
else
+
iterate lambda' lambda (iter + 1)
+
in
+
+
iterate l 0.0 0
+
+
(** Bearing calculations *)
+
+
let bearing p1 p2 =
+
let lat1 = lat_float p1 *. deg_to_rad in
+
let lat2 = lat_float p2 *. deg_to_rad in
+
let lon1 = lon_float p1 *. deg_to_rad in
+
let lon2 = lon_float p2 *. deg_to_rad in
+
+
let dlon = lon2 -. lon1 in
+
let y = sin dlon *. cos lat2 in
+
let x = cos lat1 *. sin lat2 -. sin lat1 *. cos lat2 *. cos dlon in
+
+
let bearing = atan2 y x *. rad_to_deg in
+
mod_float (bearing +. 360.0) 360.0
+
+
let bearing_final p1 p2 =
+
(* Final bearing is the reverse of the initial bearing from p2 to p1 *)
+
let reverse_bearing = bearing p2 p1 in
+
mod_float (reverse_bearing +. 180.0) 360.0
+
+
(** Geographic operations *)
+
+
let offset t ~heading ~distance =
+
let lat1 = lat_float t *. deg_to_rad in
+
let lon1 = lon_float t *. deg_to_rad in
+
let brng = heading *. deg_to_rad in
+
let d = distance /. earth_radius_km in
+
+
let lat2 = asin (sin lat1 *. cos d +. cos lat1 *. sin d *. cos brng) in
+
let lon2 = lon1 +. atan2 (sin brng *. sin d *. cos lat1)
+
(cos d -. sin lat1 *. sin lat2) in
+
+
create ~lat:(lat2 *. rad_to_deg) ~lon:(lon2 *. rad_to_deg)
+
+
let midpoint p1 p2 =
+
let lat1 = lat_float p1 *. deg_to_rad in
+
let lat2 = lat_float p2 *. deg_to_rad in
+
let lon1 = lon_float p1 *. deg_to_rad in
+
let lon2 = lon_float p2 *. deg_to_rad in
+
+
let dlon = lon2 -. lon1 in
+
let bx = cos lat2 *. cos dlon in
+
let by = cos lat2 *. sin dlon in
+
+
let lat3 = atan2 (sin lat1 +. sin lat2)
+
(sqrt ((cos lat1 +. bx) ** 2.0 +. by ** 2.0)) in
+
let lon3 = lon1 +. atan2 by (cos lat1 +. bx) in
+
+
create ~lat:(lat3 *. rad_to_deg) ~lon:(lon3 *. rad_to_deg)
+
+
let interpolate p1 p2 fraction =
+
if fraction <= 0.0 then p1
+
else if fraction >= 1.0 then p2
+
else
+
let lat1 = lat_float p1 in
+
let lat2 = lat_float p2 in
+
let lon1 = lon_float p1 in
+
let lon2 = lon_float p2 in
+
+
(* Simple linear interpolation in degrees *)
+
let lat = lat1 +. (lat2 -. lat1) *. fraction in
+
let lon = lon1 +. (lon2 -. lon1) *. fraction in
+
+
create ~lat ~lon
+
+
(** Comparison *)
+
+
let equal p1 p2 =
+
Lat.equal p1.lat p2.lat && Lon.equal p1.lon p2.lon
+
+
let ( = ) = equal
+
+
let almost_equal ?(epsilon = 0.000001) p1 p2 =
+
distance_haversine p1 p2 < epsilon
+
+
(** Formatting *)
+
+
let to_string t =
+
Printf.sprintf "%s, %s" (Lat.to_string t.lat) (Lon.to_string t.lon)
+
+
let to_dms_string t =
+
Printf.sprintf "%s, %s" (Lat.to_dms_string t.lat) (Lon.to_dms_string t.lon)
+
+
let format ?(lat_precision = 6) ?(lon_precision = 6) t =
+
Printf.sprintf "%s, %s"
+
(Lat.format ~precision:lat_precision t.lat)
+
(Lon.format ~precision:lon_precision t.lon)
+59
stack/lonlat/lib/coord.mli
···
+
(** Coordinate pair module *)
+
+
type t
+
+
(** {2 Construction} *)
+
+
val create : lat:float -> lon:float -> t
+
+
val of_lat_lon : Lat.t -> Lon.t -> t
+
+
(** {2 Accessors} *)
+
+
val lat : t -> Lat.t
+
val lon : t -> Lon.t
+
val lat_float : t -> float
+
val lon_float : t -> float
+
+
(** {2 Distance calculations} *)
+
+
val distance_haversine : t -> t -> float
+
(** Great circle distance in km using Earth radius 6371.0 *)
+
+
val distance_vincenty : t -> t -> float
+
(** WGS84 ellipsoid distance in km *)
+
+
(** {2 Bearing calculations} *)
+
+
val bearing : t -> t -> float
+
(** Initial bearing in degrees [0, 360) *)
+
+
val bearing_final : t -> t -> float
+
(** Final bearing in degrees *)
+
+
(** {2 Geographic operations} *)
+
+
val offset : t -> heading:float -> distance:float -> t
+
(** New point from heading (degrees) and distance (km) *)
+
+
val midpoint : t -> t -> t
+
+
val interpolate : t -> t -> float -> t
+
(** Linear interpolation: 0.0 = first, 1.0 = second *)
+
+
(** {2 Comparison} *)
+
+
val equal : t -> t -> bool
+
val ( = ) : t -> t -> bool
+
+
val almost_equal : ?epsilon:float -> t -> t -> bool
+
(** Default epsilon = 0.000001 km *)
+
+
(** {2 Formatting} *)
+
+
val to_string : t -> string
+
(** "lat, lon" in decimal degrees *)
+
+
val to_dms_string : t -> string
+
+
val format : ?lat_precision:int -> ?lon_precision:int -> t -> string
+5
stack/lonlat/lib/dune
···
+
(library
+
(public_name lonlat)
+
(name lonlat)
+
(modules error lat lon coord bbox vector parser lonlat)
+
(libraries str))
+33
stack/lonlat/lib/error.ml
···
+
(** Error handling for geographic coordinates *)
+
+
type t =
+
| Invalid_latitude of float
+
| Invalid_longitude of float
+
| Parse_error of string
+
| Invalid_format of string
+
| Invalid_hemisphere of string
+
+
exception Coordinate_error of t
+
+
let to_string = function
+
| Invalid_latitude f -> Printf.sprintf "Invalid latitude: %.6f (must be between -90 and 90)" f
+
| Invalid_longitude f -> Printf.sprintf "Invalid longitude: %.6f" f
+
| Parse_error s -> Printf.sprintf "Parse error: %s" s
+
| Invalid_format s -> Printf.sprintf "Invalid format: %s" s
+
| Invalid_hemisphere s -> Printf.sprintf "Invalid hemisphere: %s" s
+
+
let catch f x =
+
try Ok (f x)
+
with Coordinate_error e -> Error e
+
+
let catch_exn f x =
+
try Some (f x)
+
with Coordinate_error _ -> None
+
+
let unwrap = function
+
| Ok x -> x
+
| Error e -> raise (Coordinate_error e)
+
+
let unwrap_or default = function
+
| Ok x -> x
+
| Error _ -> default
+18
stack/lonlat/lib/error.mli
···
+
(** Error handling for geographic coordinates *)
+
+
type t =
+
| Invalid_latitude of float
+
| Invalid_longitude of float
+
| Parse_error of string
+
| Invalid_format of string
+
| Invalid_hemisphere of string
+
+
exception Coordinate_error of t
+
+
val to_string : t -> string
+
+
(** Error handling combinators *)
+
val catch : ('a -> 'b) -> 'a -> ('b, t) result
+
val catch_exn : ('a -> 'b) -> 'a -> 'b option
+
val unwrap : ('a, t) result -> 'a
+
val unwrap_or : 'a -> ('a, t) result -> 'a
+66
stack/lonlat/lib/lat.ml
···
+
(** Latitude module *)
+
+
type t = float
+
+
let is_valid f = f >= -90.0 && f <= 90.0
+
+
let create f =
+
if not (is_valid f) then
+
raise (Error.Coordinate_error (Error.Invalid_latitude f))
+
else f
+
+
let unsafe_create f = f
+
+
let clamp f =
+
if f < -90.0 then -90.0
+
else if f > 90.0 then 90.0
+
else f
+
+
let of_dms ~degree ~minute ~second =
+
let sign = if degree < 0 || minute < 0 || second < 0.0 then -1.0 else 1.0 in
+
let d = abs degree in
+
let m = abs minute in
+
let s = abs_float second in
+
let decimal = float_of_int d +. (float_of_int m /. 60.0) +. (s /. 3600.0) in
+
create (sign *. decimal)
+
+
let to_float t = t
+
+
let degree t = int_of_float t
+
+
let decimal_minute t =
+
let d = abs_float t in
+
(d -. floor d) *. 60.0
+
+
let minute t = int_of_float (decimal_minute t)
+
+
let second t =
+
let dm = decimal_minute t in
+
(dm -. floor dm) *. 60.0
+
+
let hemisphere t = if t < 0.0 then `S else `N
+
+
let add t delta = create (t +. delta)
+
let sub t delta = create (t -. delta)
+
let neg t = create (-.t)
+
let abs t = abs_float t
+
+
let equal a b = a = b
+
let compare = Float.compare
+
let ( = ) = equal
+
let ( < ) a b = compare a b < 0
+
let ( > ) a b = compare a b > 0
+
let ( <= ) a b = compare a b <= 0
+
let ( >= ) a b = compare a b >= 0
+
+
let to_string t = Printf.sprintf "%.6f" t
+
+
let format ?(precision = 6) t =
+
Printf.sprintf "%.*f" precision t
+
+
let to_dms_string t =
+
let h = match hemisphere t with `N -> "N" | `S -> "S" in
+
let d = abs_float (float_of_int (degree t)) |> int_of_float in
+
let m = minute (abs_float t) in
+
let s = second (abs_float t) in
+
Printf.sprintf "%d°%02d'%05.2f\"%s" d m s h
+57
stack/lonlat/lib/lat.mli
···
+
(** Latitude module *)
+
+
type t
+
+
(** {2 Construction} *)
+
+
val create : float -> t
+
(** Raises Error.Coordinate_error if outside [-90, 90] *)
+
+
val of_dms : degree:int -> minute:int -> second:float -> t
+
+
val unsafe_create : float -> t
+
(** No validation - use with caution *)
+
+
val clamp : float -> t
+
(** Clamp to valid range [-90, 90] *)
+
+
(** {2 Accessors} *)
+
+
val to_float : t -> float
+
val degree : t -> int
+
val minute : t -> int
+
val second : t -> float
+
val decimal_minute : t -> float
+
val hemisphere : t -> [`N | `S]
+
+
(** {2 Operations} *)
+
+
val add : t -> float -> t
+
val sub : t -> float -> t
+
val neg : t -> t
+
val abs : t -> float
+
+
(** {2 Comparison} *)
+
+
val equal : t -> t -> bool
+
val compare : t -> t -> int
+
val ( = ) : t -> t -> bool
+
val ( < ) : t -> t -> bool
+
val ( > ) : t -> t -> bool
+
val ( <= ) : t -> t -> bool
+
val ( >= ) : t -> t -> bool
+
+
(** {2 Validation} *)
+
+
val is_valid : float -> bool
+
+
(** {2 Formatting} *)
+
+
val to_string : t -> string
+
(** Decimal degrees *)
+
+
val format : ?precision:int -> t -> string
+
(** With specified precision (default 6) *)
+
+
val to_dms_string : t -> string
+
(** Format as "DD°MM'SS.S\"N" *)
+71
stack/lonlat/lib/lon.ml
···
+
(** Longitude module *)
+
+
type t = float
+
+
let normalize_180 f =
+
let x = mod_float (f +. 180.0) 360.0 in
+
if x < 0.0 then x +. 360.0 -. 180.0
+
else x -. 180.0
+
+
let normalize_360 f =
+
let x = mod_float f 360.0 in
+
if x < 0.0 then x +. 360.0 else x
+
+
let create f = normalize_180 f
+
+
let unsafe_create f = f
+
+
let of_dms ~degree ~minute ~second =
+
let sign = if degree < 0 || minute < 0 || second < 0.0 then -1.0 else 1.0 in
+
let d = abs degree in
+
let m = abs minute in
+
let s = abs_float second in
+
let decimal = float_of_int d +. (float_of_int m /. 60.0) +. (s /. 3600.0) in
+
create (sign *. decimal)
+
+
let to_float t = t
+
+
let to_range_180 t = normalize_180 t
+
let to_range_360 t = normalize_360 t
+
+
let degree t = int_of_float t
+
+
let decimal_minute t =
+
let d = abs_float t in
+
(d -. floor d) *. 60.0
+
+
let minute t = int_of_float (decimal_minute t)
+
+
let second t =
+
let dm = decimal_minute t in
+
(dm -. floor dm) *. 60.0
+
+
let hemisphere t = if t < 0.0 then `W else `E
+
+
let add t delta = create (t +. delta)
+
let sub t delta = create (t -. delta)
+
let neg t = create (-.t)
+
let abs t = abs_float t
+
+
let equal a b =
+
(* Longitudes wrap around, so normalize before comparing *)
+
normalize_180 a = normalize_180 b
+
+
let compare a b = Float.compare (normalize_180 a) (normalize_180 b)
+
let ( = ) = equal
+
let ( < ) a b = compare a b < 0
+
let ( > ) a b = compare a b > 0
+
let ( <= ) a b = compare a b <= 0
+
let ( >= ) a b = compare a b >= 0
+
+
let to_string t = Printf.sprintf "%.6f" t
+
+
let format ?(precision = 6) t =
+
Printf.sprintf "%.*f" precision t
+
+
let to_dms_string t =
+
let h = match hemisphere t with `E -> "E" | `W -> "W" in
+
let d = abs_float (float_of_int (degree t)) |> int_of_float in
+
let m = minute (abs_float t) in
+
let s = second (abs_float t) in
+
Printf.sprintf "%03d°%02d'%05.2f\"%s" d m s h
+56
stack/lonlat/lib/lon.mli
···
+
(** Longitude module *)
+
+
type t
+
+
(** {2 Construction} *)
+
+
val create : float -> t
+
(** Normalizes to [-180, 180] *)
+
+
val of_dms : degree:int -> minute:int -> second:float -> t
+
+
val unsafe_create : float -> t
+
+
(** {2 Accessors} *)
+
+
val to_float : t -> float
+
val degree : t -> int
+
val minute : t -> int
+
val second : t -> float
+
val decimal_minute : t -> float
+
val hemisphere : t -> [`E | `W]
+
+
(** {2 Range operations} *)
+
+
val normalize_180 : float -> float
+
(** Normalize any value to [-180, 180] *)
+
+
val normalize_360 : float -> float
+
(** Normalize any value to [0, 360] *)
+
+
val to_range_180 : t -> float
+
val to_range_360 : t -> float
+
+
(** {2 Operations} *)
+
+
val add : t -> float -> t
+
val sub : t -> float -> t
+
val neg : t -> t
+
val abs : t -> float
+
+
(** {2 Comparison} *)
+
+
val equal : t -> t -> bool
+
val compare : t -> t -> int
+
val ( = ) : t -> t -> bool
+
val ( < ) : t -> t -> bool
+
val ( > ) : t -> t -> bool
+
val ( <= ) : t -> t -> bool
+
val ( >= ) : t -> t -> bool
+
+
(** {2 Formatting} *)
+
+
val to_string : t -> string
+
val format : ?precision:int -> t -> string
+
val to_dms_string : t -> string
+
(** Format as "DDD°MM'SS.S\"E" *)
+15
stack/lonlat/lib/lonlat.ml
···
+
(** LonLat - Geographic coordinate manipulation library for OCaml *)
+
+
module Error = Error
+
module Lat = Lat
+
module Lon = Lon
+
module Coord = Coord
+
module Bbox = Bbox
+
module Vector = Vector
+
module Parser = Parser
+
+
(** Constants *)
+
let earth_radius_km = 6371.0
+
let wgs84_a = 6378.137
+
let wgs84_b = 6356.752
+
let wgs84_f = 1.0 /. 298.257223563
+23
stack/lonlat/lib/lonlat.mli
···
+
(** LonLat - Geographic coordinate manipulation library for OCaml *)
+
+
module Error = Error
+
module Lat = Lat
+
module Lon = Lon
+
module Coord = Coord
+
module Bbox = Bbox
+
module Vector = Vector
+
module Parser = Parser
+
+
(** {1 Constants} *)
+
+
val earth_radius_km : float
+
(** Mean Earth radius: 6371.0 km *)
+
+
val wgs84_a : float
+
(** WGS84 semi-major axis: 6378.137 km *)
+
+
val wgs84_b : float
+
(** WGS84 semi-minor axis: 6356.752 km *)
+
+
val wgs84_f : float
+
(** WGS84 flattening: 1/298.257223563 *)
+134
stack/lonlat/lib/parser.ml
···
+
(** 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
+23
stack/lonlat/lib/parser.mli
···
+
(** Coordinate parsing module *)
+
+
val parse_lat : string -> Lat.t
+
(** Parse decimal degrees or DMS formats *)
+
+
val parse_lon : string -> Lon.t
+
+
val parse_coord : string -> Coord.t
+
(** Parse "lat,lon" or "lat lon" formats *)
+
+
val parse_dms : string -> float
+
(** Parse various DMS formats:
+
- "40.7128"
+
- "40°42'46\""
+
- "40°42.767'"
+
- "40 42 46"
+
- "40:42:46"
+
- "40.7128N" or "N40.7128" *)
+
+
(** Try parsing - returns None on failure instead of raising *)
+
val try_parse_lat : string -> Lat.t option
+
val try_parse_lon : string -> Lon.t option
+
val try_parse_coord : string -> Coord.t option
+71
stack/lonlat/lib/vector.ml
···
+
(** Geographic vector module *)
+
+
type t = {
+
dx : float; (* km *)
+
dy : float; (* km *)
+
}
+
+
let deg_to_rad = Float.pi /. 180.0
+
let rad_to_deg = 180.0 /. Float.pi
+
+
let create_cartesian ~dx ~dy = { dx; dy }
+
+
let create_polar ~heading ~distance =
+
(* Convert heading to mathematical angle (east = 0, counterclockwise) *)
+
let angle = (90.0 -. heading) *. deg_to_rad in
+
{
+
dx = distance *. cos angle;
+
dy = distance *. sin angle;
+
}
+
+
let between p1 p2 =
+
(* Calculate vector between two coordinates *)
+
let dist = Coord.distance_haversine p1 p2 in
+
let bearing = Coord.bearing p1 p2 in
+
create_polar ~heading:bearing ~distance:dist
+
+
let dx t = t.dx
+
let dy t = t.dy
+
+
let heading t =
+
(* Convert from mathematical angle to geographic heading *)
+
let angle = atan2 t.dy t.dx *. rad_to_deg in
+
mod_float (90.0 -. angle +. 360.0) 360.0
+
+
let distance t =
+
sqrt (t.dx ** 2.0 +. t.dy ** 2.0)
+
+
let add v1 v2 = {
+
dx = v1.dx +. v2.dx;
+
dy = v1.dy +. v2.dy;
+
}
+
+
let sub v1 v2 = {
+
dx = v1.dx -. v2.dx;
+
dy = v1.dy -. v2.dy;
+
}
+
+
let scale v factor = {
+
dx = v.dx *. factor;
+
dy = v.dy *. factor;
+
}
+
+
let neg v = {
+
dx = -.v.dx;
+
dy = -.v.dy;
+
}
+
+
let rotate v degrees =
+
let angle = degrees *. deg_to_rad in
+
let cos_a = cos angle in
+
let sin_a = sin angle in
+
{
+
dx = v.dx *. cos_a -. v.dy *. sin_a;
+
dy = v.dx *. sin_a +. v.dy *. cos_a;
+
}
+
+
let apply coord v =
+
Coord.offset coord ~heading:(heading v) ~distance:(distance v)
+
+
let to_string t =
+
Printf.sprintf "Vector(heading=%.2f°, distance=%.2fkm)" (heading t) (distance t)
+39
stack/lonlat/lib/vector.mli
···
+
(** Geographic vector module *)
+
+
type t
+
+
(** {2 Construction} *)
+
+
val create_cartesian : dx:float -> dy:float -> t
+
(** dx, dy in kilometers *)
+
+
val create_polar : heading:float -> distance:float -> t
+
(** heading in degrees, distance in km *)
+
+
val between : Coord.t -> Coord.t -> t
+
(** Vector from first to second point *)
+
+
(** {2 Accessors} *)
+
+
val dx : t -> float
+
val dy : t -> float
+
val heading : t -> float
+
val distance : t -> float
+
+
(** {2 Operations} *)
+
+
val add : t -> t -> t
+
val sub : t -> t -> t
+
val scale : t -> float -> t
+
val neg : t -> t
+
val rotate : t -> float -> t
+
(** Rotate by degrees *)
+
+
(** {2 Application} *)
+
+
val apply : Coord.t -> t -> Coord.t
+
(** Apply vector to coordinate *)
+
+
(** {2 Formatting} *)
+
+
val to_string : t -> string
+27
stack/lonlat/lonlat.opam
···
+
# This file is generated by dune, edit dune-project instead
+
opam-version: "2.0"
+
synopsis: "Geographic coordinate manipulation library for OCaml"
+
description:
+
"LonLat provides types and operations for working with latitude/longitude coordinates, including distance calculations, bearings, bounding boxes, and coordinate parsing."
+
maintainer: ["maintainer@example.com"]
+
authors: ["LonLat Authors"]
+
license: "MIT"
+
depends: [
+
"ocaml"
+
"dune" {>= "3.0"}
+
"odoc" {with-doc}
+
]
+
build: [
+
["dune" "subst"] {dev}
+
[
+
"dune"
+
"build"
+
"-p"
+
name
+
"-j"
+
jobs
+
"@install"
+
"@runtest" {with-test}
+
"@doc" {with-doc}
+
]
+
]