///|
pub(all) struct Bounds {
min_latitude : Double
min_longitude : Double
max_latitude : Double
max_longitude : Double
} derive(Eq, Debug)
///|
pub fn valid_latitude(latitude : Double) -> Bool {
latitude >= -90.0 && latitude <= 90.0
}
///|
pub fn valid_longitude(longitude : Double) -> Bool {
longitude >= -180.0 && longitude <= 180.0
}
///|
pub fn valid_coordinate(latitude : Double, longitude : Double) -> Bool {
valid_latitude(latitude) && valid_longitude(longitude)
}
///|
pub fn maybe_reversed(latitude : Double, longitude : Double) -> Bool {
!valid_latitude(latitude) &&
latitude >= -180.0 &&
latitude <= 180.0 &&
longitude >= -90.0 &&
longitude <= 90.0
}
///|
pub fn Bounds::contains(
self : Bounds,
latitude : Double,
longitude : Double,
) -> Bool {
latitude >= self.min_latitude &&
latitude <= self.max_latitude &&
longitude >= self.min_longitude &&
longitude <= self.max_longitude
}
///|
fn deg_to_rad(value : Double) -> Double {
value / 180.0 * @math.PI
}
///|
/// Haversine distance in kilometers.
pub fn distance_km(
lat1 : Double,
lon1 : Double,
lat2 : Double,
lon2 : Double,
) -> Double {
let dlat = deg_to_rad(lat2 - lat1)
let dlon = deg_to_rad(lon2 - lon1)
let a = @math.sin(dlat / 2.0) * @math.sin(dlat / 2.0) +
@math.cos(deg_to_rad(lat1)) *
@math.cos(deg_to_rad(lat2)) *
@math.sin(dlon / 2.0) *
@math.sin(dlon / 2.0)
6371.0088 * 2.0 * @math.asin(a.sqrt())
}
///|
/// Approximate a grid-cell center. Longitude step is adjusted by latitude.
pub fn grid_center(
latitude : Double,
longitude : Double,
size_km : Double,
) -> (Double, Double) {
let safe_size = if size_km <= 0.0 { 1.0 } else { size_km }
let lat_step = safe_size / 111.32
let cos_lat = @math.cos(deg_to_rad(latitude)).abs()
let lon_km = if cos_lat < 0.01 { 111.32 } else { 111.32 * cos_lat }
let lon_step = safe_size / lon_km
let lat_cell = (latitude / lat_step).floor()
let lon_cell = (longitude / lon_step).floor()
(lat_cell * lat_step + lat_step / 2.0, lon_cell * lon_step + lon_step / 2.0)
}
///|
pub fn round_to_decimals(value : Double, decimals : Int) -> Double {
let mut factor = 1.0
let mut i = 0
while i < decimals {
factor = factor * 10.0
i = i + 1
}
(value * factor).round() / factor
}
///|
fn pseudo_unit(seed : String) -> Double {
let mut hash = 2166136261U
seed
.iter()
.each(ch => {
hash = hash ^ ch.to_uint()
hash = hash * 16777619U
})
(hash % 1000000U).reinterpret_as_int().to_double() / 1000000.0
}
///|
/// Deterministic jitter for reproducible public datasets.
pub fn jitter(
latitude : Double,
longitude : Double,
radius_km : Double,
seed : String,
) -> (Double, Double) {
let radius = if radius_km <= 0.0 { 1.0 } else { radius_km }
let u = pseudo_unit(seed + ":u")
let v = pseudo_unit(seed + ":v")
let bearing = 2.0 * @math.PI * u
let distance = radius * v.sqrt()
let lat_delta = @math.cos(bearing) * distance / 111.32
let cos_lat = @math.cos(deg_to_rad(latitude)).abs()
let lon_scale = if cos_lat < 0.01 { 111.32 } else { 111.32 * cos_lat }
let lon_delta = @math.sin(bearing) * distance / lon_scale
(latitude + lat_delta, longitude + lon_delta)
}
///|
pub fn coordinate_precision_decimals(text : String) -> Int {
let normalized = text.trim().to_owned()
let parts = normalized.split(".").to_array()
if parts.length() == 2 {
parts[1].trim().length()
} else {
0
}
}