use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use std::str::FromStr;
use super::error::{OtomlError, Result};
const EARTH_RADIUS_METERS: f64 = 6_371_000.0;
const MICRO_DEGREES: i32 = 1_000_000;
const MAX_LAT_MICRO: i32 = 90_000_000;
const MAX_LON_MICRO: i32 = 180_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OLocation {
lat_micro: i32,
lon_micro: i32,
radius: u16,
}
impl OLocation {
pub fn new(lat: f64, lon: f64, radius: u16) -> Result<Self> {
if !(-90.0..=90.0).contains(&lat) {
return Err(OtomlError::InvalidLocation(format!(
"latitude {} out of range (-90 to 90)",
lat
)));
}
if !(-180.0..=180.0).contains(&lon) {
return Err(OtomlError::InvalidLocation(format!(
"longitude {} out of range (-180 to 180)",
lon
)));
}
if lat.is_nan() || lat.is_infinite() {
return Err(OtomlError::InvalidLocation(
"latitude cannot be NaN or infinity".to_string(),
));
}
if lon.is_nan() || lon.is_infinite() {
return Err(OtomlError::InvalidLocation(
"longitude cannot be NaN or infinity".to_string(),
));
}
let lat_micro = round_half_away_from_zero(lat * MICRO_DEGREES as f64) as i32;
let lon_micro = round_half_away_from_zero(lon * MICRO_DEGREES as f64) as i32;
Ok(OLocation {
lat_micro,
lon_micro,
radius,
})
}
pub fn from_micro(lat_micro: i32, lon_micro: i32, radius: u16) -> Result<Self> {
if !(-MAX_LAT_MICRO..=MAX_LAT_MICRO).contains(&lat_micro) {
return Err(OtomlError::InvalidLocation(format!(
"latitude micro-degrees {} out of range",
lat_micro
)));
}
if !(-MAX_LON_MICRO..=MAX_LON_MICRO).contains(&lon_micro) {
return Err(OtomlError::InvalidLocation(format!(
"longitude micro-degrees {} out of range",
lon_micro
)));
}
Ok(OLocation {
lat_micro,
lon_micro,
radius,
})
}
pub fn lat(&self) -> f64 {
self.lat_micro as f64 / MICRO_DEGREES as f64
}
pub fn lon(&self) -> f64 {
self.lon_micro as f64 / MICRO_DEGREES as f64
}
pub fn lat_micro(&self) -> i32 {
self.lat_micro
}
pub fn lon_micro(&self) -> i32 {
self.lon_micro
}
pub fn radius(&self) -> u16 {
self.radius
}
pub fn is_exact(&self) -> bool {
self.radius == 0
}
pub fn distance(&self, other: &OLocation) -> f64 {
haversine_distance(self.lat(), self.lon(), other.lat(), other.lon())
}
pub fn distance_with_uncertainty(&self, other: &OLocation) -> f64 {
let center_distance = self.distance(other);
let combined_radius = self.radius as f64 + other.radius as f64;
if center_distance <= combined_radius {
0.0
} else {
center_distance - combined_radius
}
}
pub fn overlaps(&self, other: &OLocation) -> bool {
self.distance_with_uncertainty(other) == 0.0
}
pub fn contains_point(&self, lat: f64, lon: f64) -> Result<bool> {
let point = OLocation::new(lat, lon, 0)?;
Ok(self.distance(&point) <= self.radius as f64)
}
}
impl Default for OLocation {
fn default() -> Self {
OLocation {
lat_micro: 0,
lon_micro: 0,
radius: 0,
}
}
}
impl fmt::Display for OLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let lat = self.lat();
let lon = self.lon();
let lat_str = format_decimal(lat, 6);
let lon_str = format_decimal(lon, 6);
write!(f, "({},{},{})", lat_str, lon_str, self.radius)
}
}
impl FromStr for OLocation {
type Err = OtomlError;
fn from_str(s: &str) -> Result<Self> {
let s = s.trim();
if !s.starts_with('(') || !s.ends_with(')') {
return Err(OtomlError::InvalidLocation(format!(
"invalid format, expected (lat,lon,radius), got '{}'",
s
)));
}
let inner = &s[1..s.len() - 1];
let parts: Vec<&str> = inner.split(',').collect();
if parts.len() != 3 {
return Err(OtomlError::InvalidLocation(format!(
"expected 3 components (lat,lon,radius), got {}",
parts.len()
)));
}
let lat: f64 = parts[0]
.trim()
.parse()
.map_err(|_| OtomlError::InvalidLocation(format!("invalid latitude '{}'", parts[0])))?;
let lon: f64 = parts[1].trim().parse().map_err(|_| {
OtomlError::InvalidLocation(format!("invalid longitude '{}'", parts[1]))
})?;
let radius: u16 = parts[2]
.trim()
.parse()
.map_err(|_| OtomlError::InvalidLocation(format!("invalid radius '{}'", parts[2])))?;
OLocation::new(lat, lon, radius)
}
}
impl Ord for OLocation {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.lat_micro
.cmp(&other.lat_micro)
.then(self.lon_micro.cmp(&other.lon_micro))
.then(self.radius.cmp(&other.radius))
}
}
impl PartialOrd for OLocation {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Serialize for OLocation {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for OLocation {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
OLocation::from_str(&s).map_err(serde::de::Error::custom)
}
}
fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
let lat1_rad = lat1.to_radians();
let lat2_rad = lat2.to_radians();
let delta_lat = (lat2 - lat1).to_radians();
let delta_lon = (lon2 - lon1).to_radians();
let a = (delta_lat / 2.0).sin().powi(2)
+ lat1_rad.cos() * lat2_rad.cos() * (delta_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().asin();
EARTH_RADIUS_METERS * c
}
fn round_half_away_from_zero(x: f64) -> f64 {
if x >= 0.0 {
(x + 0.5).floor()
} else {
(x - 0.5).ceil()
}
}
fn format_decimal(value: f64, max_decimals: usize) -> String {
let formatted = format!("{:.prec$}", value, prec = max_decimals);
if formatted.contains('.') {
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
if trimmed.is_empty() || trimmed == "-" {
"0".to_string()
} else {
trimmed.to_string()
}
} else {
formatted
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new() {
let loc = OLocation::new(47.376887, 8.541694, 1000).unwrap();
assert!((loc.lat() - 47.376887).abs() < 0.000001);
assert!((loc.lon() - 8.541694).abs() < 0.000001);
assert_eq!(loc.radius(), 1000);
}
#[test]
fn test_display() {
let loc = OLocation::new(47.376887, 8.541694, 1000).unwrap();
assert_eq!(loc.to_string(), "(47.376887,8.541694,1000)");
let loc = OLocation::new(-33.86882, 151.209296, 3000).unwrap();
assert_eq!(loc.to_string(), "(-33.86882,151.209296,3000)");
let loc = OLocation::new(0.0, 0.0, 0).unwrap();
assert_eq!(loc.to_string(), "(0,0,0)");
}
#[test]
fn test_parse() {
let loc: OLocation = "(47.376887,8.541694,5)".parse().unwrap();
assert!((loc.lat() - 47.376887).abs() < 0.000001);
assert!((loc.lon() - 8.541694).abs() < 0.000001);
assert_eq!(loc.radius(), 5);
let loc: OLocation = "( 51.507351 , -0.127758 , 1000 )".parse().unwrap();
assert!((loc.lat() - 51.507351).abs() < 0.000001);
let loc: OLocation = "(-33.868820,151.209296,3000)".parse().unwrap();
assert!((loc.lat() - -33.86882).abs() < 0.000001);
}
#[test]
fn test_roundtrip() {
let original = "(47.376887,8.541694,1000)";
let loc: OLocation = original.parse().unwrap();
assert_eq!(loc.to_string(), original);
}
#[test]
fn test_validation() {
assert!(OLocation::new(90.0, 180.0, 0).is_ok());
assert!(OLocation::new(-90.0, -180.0, 65535).is_ok());
assert!(OLocation::new(91.0, 0.0, 0).is_err());
assert!(OLocation::new(-91.0, 0.0, 0).is_err());
assert!(OLocation::new(0.0, 181.0, 0).is_err());
assert!(OLocation::new(0.0, -181.0, 0).is_err());
assert!(OLocation::new(f64::NAN, 0.0, 0).is_err());
assert!(OLocation::new(0.0, f64::INFINITY, 0).is_err());
}
#[test]
fn test_distance_same_point() {
let loc = OLocation::new(47.376887, 8.541694, 0).unwrap();
assert_eq!(loc.distance(&loc), 0.0);
}
#[test]
fn test_distance_zurich_london() {
let zurich = OLocation::new(47.376887, 8.541694, 0).unwrap();
let london = OLocation::new(51.507351, -0.127758, 0).unwrap();
let distance_km = zurich.distance(&london) / 1000.0;
assert!(
(distance_km - 778.0).abs() < 5.0,
"Expected ~778 km, got {} km",
distance_km
);
}
#[test]
fn test_distance_antipodal() {
let north = OLocation::new(90.0, 0.0, 0).unwrap();
let south = OLocation::new(-90.0, 0.0, 0).unwrap();
let distance_km = north.distance(&south) / 1000.0;
assert!(
(distance_km - 20015.0).abs() < 100.0,
"Expected ~20015 km, got {} km",
distance_km
);
}
#[test]
fn test_distance_equator() {
let p1 = OLocation::new(0.0, 0.0, 0).unwrap();
let p2 = OLocation::new(0.0, 90.0, 0).unwrap();
let distance_km = p1.distance(&p2) / 1000.0;
assert!(
(distance_km - 10007.5).abs() < 50.0,
"Expected ~10007.5 km, got {} km",
distance_km
);
}
#[test]
fn test_distance_with_uncertainty() {
let loc1 = OLocation::new(47.376887, 8.541694, 1000).unwrap();
let loc2 = OLocation::new(47.376887, 8.541694, 500).unwrap();
assert_eq!(loc1.distance_with_uncertainty(&loc2), 0.0);
assert!(loc1.overlaps(&loc2));
}
#[test]
fn test_distance_with_uncertainty_no_overlap() {
let zurich = OLocation::new(47.376887, 8.541694, 1000).unwrap();
let london = OLocation::new(51.507351, -0.127758, 1000).unwrap();
let center_dist = zurich.distance(&london);
let min_dist = zurich.distance_with_uncertainty(&london);
assert!((min_dist - (center_dist - 2000.0)).abs() < 1.0);
assert!(!zurich.overlaps(&london));
}
#[test]
fn test_contains_point() {
let loc = OLocation::new(47.376887, 8.541694, 1000).unwrap();
assert!(loc.contains_point(47.376887, 8.541694).unwrap());
assert!(loc.contains_point(47.377, 8.542).unwrap());
assert!(!loc.contains_point(48.0, 9.0).unwrap());
}
#[test]
fn test_is_exact() {
let exact = OLocation::new(47.376887, 8.541694, 0).unwrap();
assert!(exact.is_exact());
let uncertain = OLocation::new(47.376887, 8.541694, 100).unwrap();
assert!(!uncertain.is_exact());
}
#[test]
fn test_ordering() {
let loc1 = OLocation::new(47.0, 8.0, 100).unwrap();
let loc2 = OLocation::new(48.0, 8.0, 100).unwrap();
let loc3 = OLocation::new(47.0, 9.0, 100).unwrap();
let loc4 = OLocation::new(47.0, 8.0, 200).unwrap();
assert!(loc1 < loc2); assert!(loc1 < loc3); assert!(loc1 < loc4); }
#[test]
fn test_from_micro() {
let loc = OLocation::from_micro(47_376_887, 8_541_694, 1000).unwrap();
assert_eq!(loc.lat_micro(), 47_376_887);
assert_eq!(loc.lon_micro(), 8_541_694);
assert!((loc.lat() - 47.376887).abs() < 0.000001);
}
#[test]
fn test_serde_roundtrip() {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Event {
name: String,
location: OLocation,
}
let event = Event {
name: "Meeting".to_string(),
location: OLocation::new(47.376887, 8.541694, 1000).unwrap(),
};
let otoml = crate::dump_otoml(&event).unwrap();
assert!(otoml.contains("location = \"(47.376887,8.541694,1000)\""));
let parsed: Event = crate::load_otoml(&otoml).unwrap();
assert_eq!(event, parsed);
}
#[test]
fn test_micro_degree_precision() {
let p1 = OLocation::new(0.0, 0.0, 0).unwrap();
let p2 = OLocation::new(0.000001, 0.0, 0).unwrap();
let distance_cm = p1.distance(&p2) * 100.0;
assert!(
(distance_cm - 11.1).abs() < 1.0,
"Expected ~11.1 cm, got {} cm",
distance_cm
);
}
#[test]
fn test_format_decimal() {
assert_eq!(format_decimal(47.376887, 6), "47.376887");
assert_eq!(format_decimal(47.0, 6), "47");
assert_eq!(format_decimal(47.5, 6), "47.5");
assert_eq!(format_decimal(-33.86882, 6), "-33.86882");
assert_eq!(format_decimal(0.0, 6), "0");
}
#[test]
fn test_known_cities() {
let cities = vec![
("Zurich", 47.376887, 8.541694),
("London", 51.507351, -0.127758),
("Sydney", -33.868820, 151.209296),
("New York", 40.712776, -74.005974),
("Tokyo", 35.689487, 139.691711),
];
for (name, lat, lon) in cities {
let loc = OLocation::new(lat, lon, 100).unwrap();
assert!(
(loc.lat() - lat).abs() < 0.000001,
"{} latitude mismatch",
name
);
assert!(
(loc.lon() - lon).abs() < 0.000001,
"{} longitude mismatch",
name
);
}
}
#[test]
fn test_binary_roundtrip() {
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Poi {
name: String,
location: OLocation,
}
let poi = Poi {
name: "Zurich HB".to_string(),
location: OLocation::new(47.378177, 8.540192, 50).unwrap(),
};
let bytes = crate::dump_obin(&poi).unwrap();
let parsed: Poi = crate::load_obin(&bytes).unwrap();
assert_eq!(poi, parsed);
}
#[test]
fn test_default() {
let loc = OLocation::default();
assert_eq!(loc.lat(), 0.0);
assert_eq!(loc.lon(), 0.0);
assert_eq!(loc.radius(), 0);
assert!(loc.is_exact());
}
#[test]
fn test_hash() {
use std::collections::HashSet;
let l1 = OLocation::new(47.376887, 8.541694, 100).unwrap();
let l2 = OLocation::new(47.376887, 8.541694, 100).unwrap();
let l3 = OLocation::new(47.376887, 8.541694, 200).unwrap();
let mut set = HashSet::new();
set.insert(l1);
set.insert(l2); set.insert(l3);
assert_eq!(set.len(), 2);
}
#[test]
fn test_clone_and_copy() {
let l1 = OLocation::new(47.376887, 8.541694, 100).unwrap();
let l2 = l1; let l3 = l1.clone();
assert_eq!(l1, l2);
assert_eq!(l1, l3);
}
#[test]
fn test_extreme_coordinates() {
let north = OLocation::new(90.0, 0.0, 0).unwrap();
assert_eq!(north.lat(), 90.0);
let south = OLocation::new(-90.0, 0.0, 0).unwrap();
assert_eq!(south.lat(), -90.0);
let west = OLocation::new(0.0, -180.0, 0).unwrap();
assert_eq!(west.lon(), -180.0);
let east = OLocation::new(0.0, 180.0, 0).unwrap();
assert_eq!(east.lon(), 180.0);
let null = OLocation::new(0.0, 0.0, 0).unwrap();
assert_eq!(null.lat(), 0.0);
assert_eq!(null.lon(), 0.0);
}
#[test]
fn test_max_radius() {
let loc = OLocation::new(0.0, 0.0, u16::MAX).unwrap();
assert_eq!(loc.radius(), 65535);
}
#[test]
fn test_overlaps_edge_cases() {
let l1 = OLocation::new(0.0, 0.0, 1000).unwrap();
let l2 = OLocation::new(0.0, 0.018, 1000).unwrap();
let center_dist = l1.distance(&l2);
println!("Center distance: {} m", center_dist);
let combined = l1.radius() as f64 + l2.radius() as f64;
if center_dist <= combined {
assert!(l1.overlaps(&l2));
} else {
assert!(!l1.overlaps(&l2));
}
}
#[test]
fn test_from_micro_edge_cases() {
let max_lat = OLocation::from_micro(90_000_000, 0, 0).unwrap();
assert_eq!(max_lat.lat(), 90.0);
let min_lat = OLocation::from_micro(-90_000_000, 0, 0).unwrap();
assert_eq!(min_lat.lat(), -90.0);
let max_lon = OLocation::from_micro(0, 180_000_000, 0).unwrap();
assert_eq!(max_lon.lon(), 180.0);
let min_lon = OLocation::from_micro(0, -180_000_000, 0).unwrap();
assert_eq!(min_lon.lon(), -180.0);
assert!(OLocation::from_micro(91_000_000, 0, 0).is_err());
assert!(OLocation::from_micro(0, 181_000_000, 0).is_err());
}
#[test]
fn test_negative_coordinates() {
let sydney = OLocation::new(-33.868820, 151.209296, 100).unwrap();
assert!(sydney.lat() < 0.0);
assert!(sydney.lon() > 0.0);
let new_york = OLocation::new(40.712776, -74.005974, 100).unwrap();
assert!(new_york.lat() > 0.0);
assert!(new_york.lon() < 0.0);
let buenos_aires = OLocation::new(-34.603722, -58.381592, 100).unwrap();
assert!(buenos_aires.lat() < 0.0);
assert!(buenos_aires.lon() < 0.0);
}
#[test]
fn test_contains_point_edge() {
let loc = OLocation::new(0.0, 0.0, 1000).unwrap();
let angles: [f64; 8] = [0.0, 45.0, 90.0, 135.0, 180.0, 225.0, 270.0, 315.0];
for angle in angles {
let rad = angle.to_radians();
let delta: f64 = 0.0045; let lat = delta * rad.cos();
let lon = delta * rad.sin();
assert!(
loc.contains_point(lat, lon).unwrap(),
"Point at {}° should be contained",
angle
);
}
}
}