#[cfg(feature = "alloc")]
use alloc::string::String;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GeoContext {
pub position: Option<(f64, f64)>,
pub country_code: Option<String>,
pub region: Option<String>,
pub city: Option<String>,
pub grid_id: Option<u64>,
pub bbox: Option<[f64; 4]>,
pub epsg_code: u32,
pub accuracy_meters: Option<f32>,
pub altitude_meters: Option<f32>,
}
impl GeoContext {
#[must_use]
pub const fn new() -> Self {
Self {
position: None,
country_code: None,
region: None,
city: None,
grid_id: None,
bbox: None,
epsg_code: 4326, accuracy_meters: None,
altitude_meters: None,
}
}
#[must_use]
pub const fn from_coords(lon: f64, lat: f64) -> Self {
Self {
position: Some((lon, lat)),
country_code: None,
region: None,
city: None,
grid_id: None,
bbox: None,
epsg_code: 4326,
accuracy_meters: None,
altitude_meters: None,
}
}
#[must_use]
pub fn with_country(mut self, code: impl Into<String>) -> Self {
self.country_code = Some(code.into());
self
}
#[must_use]
pub fn with_region(mut self, region: impl Into<String>) -> Self {
self.region = Some(region.into());
self
}
#[must_use]
pub const fn with_bbox(mut self, min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
self.bbox = Some([min_x, min_y, max_x, max_y]);
self
}
#[must_use]
pub fn contains_point(&self, lon: f64, lat: f64) -> bool {
if let Some([min_x, min_y, max_x, max_y]) = self.bbox {
lon >= min_x && lon <= max_x && lat >= min_y && lat <= max_y
} else {
false
}
}
#[must_use]
pub fn distance_to(&self, lon: f64, lat: f64) -> Option<f64> {
let (self_lon, self_lat) = self.position?;
const EARTH_RADIUS_KM: f64 = 6371.0;
let lat1 = self_lat.to_radians();
let lat2 = lat.to_radians();
let delta_lat = (lat - self_lat).to_radians();
let delta_lon = (lon - self_lon).to_radians();
let a = (delta_lat / 2.0).sin().powi(2)
+ lat1.cos() * lat2.cos() * (delta_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().asin();
Some(EARTH_RADIUS_KM * c)
}
#[must_use]
pub fn is_eu_region(&self) -> bool {
const EU_COUNTRIES: &[&str] = &[
"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE",
"IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE",
"IS", "LI", "NO",
];
if let Some(ref code) = self.country_code {
EU_COUNTRIES.contains(&code.as_str())
} else {
false
}
}
#[must_use]
pub fn data_residency_tier(&self) -> DataResidencyTier {
if let Some(ref code) = self.country_code {
match code.as_str() {
"US" | "CA" => DataResidencyTier::Americas,
"JP" | "CN" | "KR" | "SG" | "AU" | "NZ" | "IN" => DataResidencyTier::AsiaPacific,
_ if self.is_eu_region() => DataResidencyTier::Europe,
_ => DataResidencyTier::Other,
}
} else {
DataResidencyTier::Unknown
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataResidencyTier {
Europe,
Americas,
AsiaPacific,
Other,
Unknown,
}
#[cfg(feature = "geo")]
impl GeoContext {
#[must_use]
pub fn from_oxigdal_bbox(bbox: &oxigdal_core::BoundingBox) -> Self {
Self {
position: Some((
f64::midpoint(bbox.min_x, bbox.max_x),
f64::midpoint(bbox.min_y, bbox.max_y),
)),
country_code: None,
region: None,
city: None,
grid_id: None,
bbox: Some([bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y]),
epsg_code: 4326,
accuracy_meters: None,
altitude_meters: None,
}
}
#[must_use]
pub fn to_oxigdal_bbox(&self) -> Option<oxigdal_core::BoundingBox> {
if let Some([min_x, min_y, max_x, max_y]) = self.bbox {
oxigdal_core::BoundingBox::new(min_x, min_y, max_x, max_y).ok()
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_geo_context_creation() {
let ctx = GeoContext::from_coords(-122.4194, 37.7749)
.with_country("US")
.with_region("California");
assert_eq!(ctx.country_code, Some("US".to_string()));
assert!(!ctx.is_eu_region());
}
#[test]
fn test_distance_calculation() {
let ctx = GeoContext::from_coords(0.0, 51.5); let distance = ctx.distance_to(-0.1276, 51.5074).unwrap();
assert!(distance < 20.0); }
#[test]
fn test_eu_detection() {
let ctx = GeoContext::new().with_country("DE");
assert!(ctx.is_eu_region());
let ctx = GeoContext::new().with_country("US");
assert!(!ctx.is_eu_region());
}
#[test]
fn test_data_residency() {
assert_eq!(
GeoContext::new().with_country("DE").data_residency_tier(),
DataResidencyTier::Europe
);
assert_eq!(
GeoContext::new().with_country("US").data_residency_tier(),
DataResidencyTier::Americas
);
assert_eq!(
GeoContext::new().with_country("JP").data_residency_tier(),
DataResidencyTier::AsiaPacific
);
}
}