use smol_str::SmolStr;
#[cfg_attr(
feature = "quickcheck",
derive(::quickcheck_richderive::Arbitrary),
quickcheck(arbitrary = "crate::quickcheck_helpers::composite::geo_location")
)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GeoLocation {
lat: f64,
lon: f64,
altitude: Option<f32>,
}
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
const _: () = {
use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeStruct};
impl Serialize for GeoLocation {
fn serialize<S: Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
let len = 2 + usize::from(self.altitude.is_some());
let mut st = ser.serialize_struct("GeoLocation", len)?;
st.serialize_field("lat", &self.lat)?;
st.serialize_field("lon", &self.lon)?;
match &self.altitude {
Some(alt) => st.serialize_field("altitude", alt)?,
None => st.skip_field("altitude")?,
}
st.end()
}
}
#[derive(Deserialize)]
struct Shadow {
lat: f64,
lon: f64,
#[serde(default)]
altitude: Option<f32>,
}
impl<'de> Deserialize<'de> for GeoLocation {
fn deserialize<D: Deserializer<'de>>(de: D) -> Result<Self, D::Error> {
let s = Shadow::deserialize(de)?;
GeoLocation::try_new(s.lat, s.lon, s.altitude).map_err(serde::de::Error::custom)
}
}
};
impl Default for GeoLocation {
#[cfg_attr(not(tarpaulin), inline(always))]
fn default() -> Self {
Self {
lat: 0.0,
lon: 0.0,
altitude: None,
}
}
}
impl GeoLocation {
pub fn try_new(lat: f64, lon: f64, altitude: Option<f32>) -> Result<Self, GeoLocationError> {
if !lat.is_finite() || !(-90.0..=90.0).contains(&lat) {
return Err(GeoLocationError::LatOutOfRange(lat));
}
if !lon.is_finite() || !(-180.0..=180.0).contains(&lon) {
return Err(GeoLocationError::LonOutOfRange(lon));
}
Ok(Self {
lat,
lon,
altitude: normalize_altitude(altitude),
})
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn lat(&self) -> f64 {
self.lat
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn lon(&self) -> f64 {
self.lon
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn altitude(&self) -> Option<f32> {
self.altitude
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn set_altitude(&mut self, altitude: f32) -> &mut Self {
self.altitude = normalize_altitude(Some(altitude));
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn update_altitude(&mut self, altitude: Option<f32>) -> &mut Self {
self.altitude = normalize_altitude(altitude);
self
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn clear_altitude(&mut self) -> &mut Self {
self.altitude = None;
self
}
#[must_use]
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn with_altitude(mut self, altitude: f32) -> Self {
self.altitude = normalize_altitude(Some(altitude));
self
}
#[must_use]
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn maybe_altitude(mut self, altitude: Option<f32>) -> Self {
self.altitude = normalize_altitude(altitude);
self
}
pub fn from_iso6709(s: &str) -> Result<Self, GeoLocationError> {
let Some(body) = s.strip_suffix('/') else {
return Err(GeoLocationError::Iso6709Malformed(SmolStr::from(s)));
};
let bytes = body.as_bytes();
if bytes.is_empty() || (bytes[0] != b'+' && bytes[0] != b'-') {
return Err(GeoLocationError::Iso6709Malformed(SmolStr::from(s)));
}
let lon_start = match next_sign(bytes, 1) {
Some(i) => i,
None => return Err(GeoLocationError::Iso6709Malformed(SmolStr::from(s))),
};
let lat_tok = &body[..lon_start];
let alt_start = next_sign(bytes, lon_start + 1);
let (lon_tok, alt_tok) = match alt_start {
Some(i) => (&body[lon_start..i], Some(&body[i..])),
None => (&body[lon_start..], None),
};
let lat = parse_signed_fixed(lat_tok, 2)
.ok_or_else(|| GeoLocationError::Iso6709Malformed(SmolStr::from(s)))?;
let lon = parse_signed_fixed(lon_tok, 3)
.ok_or_else(|| GeoLocationError::Iso6709Malformed(SmolStr::from(s)))?;
let altitude = match alt_tok {
Some(tok) => Some(
parse_signed_altitude(tok)
.ok_or_else(|| GeoLocationError::Iso6709Malformed(SmolStr::from(s)))? as f32,
),
None => None,
};
Self::try_new(lat, lon, altitude)
}
pub fn to_iso6709(&self) -> std::string::String {
use core::fmt::Write as _;
let mut out = std::string::String::with_capacity(24);
let (lat_sign, lat_int, lat_frac) = split_signed_fixed(self.lat, 4);
let _ = write!(&mut out, "{}{:02}.{:04}", lat_sign, lat_int, lat_frac);
let (lon_sign, lon_int, lon_frac) = split_signed_fixed(self.lon, 4);
let _ = write!(&mut out, "{}{:03}.{:04}", lon_sign, lon_int, lon_frac);
if let Some(alt) = self.altitude {
let neg = is_negative_f32(alt);
let mag = if neg { -(alt as f64) } else { alt as f64 };
let rounded = (mag + 0.5) as u64;
let _ = write!(&mut out, "{}{}", if neg { '-' } else { '+' }, rounded);
}
out.push('/');
out
}
}
fn split_signed_fixed(v: f64, frac_digits: u32) -> (char, u64, u64) {
let neg = is_negative_f64(v);
let mag = if neg { -v } else { v };
let int_part = mag as u64;
let frac_part_f = mag - (int_part as f64);
let scale = pow10_u64(frac_digits) as f64;
let scaled = (frac_part_f * scale + 0.5) as u64;
let frac = scaled.min(pow10_u64(frac_digits) - 1);
(if neg { '-' } else { '+' }, int_part, frac)
}
fn is_negative_f64(v: f64) -> bool {
v.to_bits() & (1u64 << 63) != 0
}
fn is_negative_f32(v: f32) -> bool {
v.to_bits() & (1u32 << 31) != 0
}
const fn pow10_u64(n: u32) -> u64 {
10u64.pow(n)
}
impl core::str::FromStr for GeoLocation {
type Err = GeoLocationError;
#[cfg_attr(not(tarpaulin), inline(always))]
fn from_str(s: &str) -> Result<Self, GeoLocationError> {
Self::from_iso6709(s)
}
}
impl core::fmt::Display for GeoLocation {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(&self.to_iso6709())
}
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum GeoLocationError {
#[error("latitude out of range [-90, 90]: {0}")]
LatOutOfRange(f64),
#[error("longitude out of range [-180, 180]: {0}")]
LonOutOfRange(f64),
#[error("malformed ISO 6709 location string: {0:?}")]
Iso6709Malformed(SmolStr),
}
fn next_sign(bytes: &[u8], from: usize) -> Option<usize> {
let mut i = from;
while i < bytes.len() {
if bytes[i] == b'+' || bytes[i] == b'-' {
return Some(i);
}
i += 1;
}
None
}
#[cfg_attr(not(tarpaulin), inline(always))]
const fn normalize_altitude(altitude: Option<f32>) -> Option<f32> {
match altitude {
Some(v) if v.is_finite() => Some(v),
_ => None,
}
}
fn parse_signed_fixed(tok: &str, int_digits: usize) -> Option<f64> {
let bytes = tok.as_bytes();
if bytes.len() < 1 + int_digits {
return None;
}
let neg = match bytes[0] {
b'+' => false,
b'-' => true,
_ => return None,
};
let int_end = 1 + int_digits;
let int_slice = &tok[1..int_end];
if int_slice.bytes().any(|b| !b.is_ascii_digit()) {
return None;
}
let frac = if int_end == tok.len() {
0.0
} else {
let rest = &tok[int_end..];
let rest_bytes = rest.as_bytes();
if rest_bytes[0] != b'.' || rest_bytes.len() < 2 {
return None;
}
let frac_str = &rest[1..];
if frac_str.bytes().any(|b| !b.is_ascii_digit()) {
return None;
}
frac_str.parse::<f64>().ok()? / pow10_u64(frac_str.len() as u32) as f64
};
let int_val: f64 = int_slice.parse::<u64>().ok()? as f64;
let mag = int_val + frac;
Some(if neg { -mag } else { mag })
}
fn parse_signed_altitude(tok: &str) -> Option<f64> {
let bytes = tok.as_bytes();
if bytes.len() < 2 {
return None;
}
let neg = match bytes[0] {
b'+' => false,
b'-' => true,
_ => return None,
};
let body = &tok[1..];
let (int_part, frac_part) = match body.find('.') {
Some(i) => {
let (lhs, rhs) = body.split_at(i);
(lhs, &rhs[1..])
}
None => (body, ""),
};
if int_part.is_empty() || int_part.bytes().any(|b| !b.is_ascii_digit()) {
return None;
}
if !frac_part.is_empty() && frac_part.bytes().any(|b| !b.is_ascii_digit()) {
return None;
}
let int_val: f64 = int_part.parse::<u64>().ok()? as f64;
let frac = if frac_part.is_empty() {
0.0
} else {
frac_part.parse::<f64>().ok()? / pow10_u64(frac_part.len() as u32) as f64
};
let mag = int_val + frac;
Some(if neg { -mag } else { mag })
}
#[cfg(test)]
mod tests;