#![no_std]
#![warn(missing_docs)]
#![allow(clippy::style)]
use core::fmt;
const MAX_LAT: f64 = 90.0;
const MIN_LAT: f64 = -90.0;
const MAX_LON: f64 = 180.0;
const MIN_LON: f64 = -180.0;
#[cfg(feature = "serde")]
mod serde;
mod codec;
mod math;
pub use codec::{Codec, GeoHash, DecodeError};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CoordinateError {
InvalidLatitude(f64),
InvalidLongitude(f64),
}
impl fmt::Display for CoordinateError {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidLatitude(value) => fmt.write_fmt(format_args!("Invalid latitude='{value}'. Allowed values are {MIN_LAT}..={MAX_LAT}")),
Self::InvalidLongitude(value) => fmt.write_fmt(format_args!("Invalid longitude='{value}'. Allowed values are {MIN_LON}..={MAX_LON}")),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Coordinate {
latitude: f64,
longitude: f64,
}
impl Coordinate {
#[inline(always)]
pub const fn new(latitude: f64, longitude: f64) -> Self {
match Self::try_new(latitude, longitude) {
Ok(result) => result,
Err(_) => panic!("Invalid coordinates"),
}
}
#[inline(always)]
pub const fn try_new(latitude: f64, longitude: f64) -> Result<Self, CoordinateError> {
if latitude.is_nan() || latitude < MIN_LAT || latitude > MAX_LAT {
Err(CoordinateError::InvalidLatitude(latitude))
} else if longitude.is_nan() || longitude < MIN_LON || longitude > MAX_LON {
Err(CoordinateError::InvalidLongitude(latitude))
} else {
Ok(Self {
latitude,
longitude
})
}
}
#[inline(always)]
pub const fn latitude(&self) -> f64 {
self.latitude
}
#[inline(always)]
pub const fn longitude(&self) -> f64 {
self.longitude
}
}
impl PartialEq<Coordinate> for &'_ Coordinate {
#[inline(always)]
fn eq(&self, other: &Coordinate) -> bool {
PartialEq::eq(*self, other)
}
}
impl PartialEq<&Coordinate> for Coordinate {
#[inline(always)]
fn eq(&self, other: &&Coordinate) -> bool {
PartialEq::eq(self, *other)
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct GeoHashPosition {
pub coord: Coordinate,
pub lat_err: f64,
pub lon_err: f64,
}
impl GeoHashPosition {
#[inline(always)]
pub const fn coordinates(&self) -> Coordinate {
self.coord
}
pub const fn neighbor(&self, direction: Direction) -> Coordinate {
let (direction_lat, direction_lon) = direction.to_lat_lon();
Coordinate {
longitude: math::rem_euclid((self.coord.longitude + 2f64 * self.lon_err.abs() * direction_lon) + MAX_LON, MAX_LON * 2.0) - MAX_LON,
latitude: math::rem_euclid((self.coord.latitude + 2f64 * self.lat_err.abs() * direction_lat) + MAX_LAT, MAX_LAT * 2.0) - MAX_LAT,
}
}
pub const fn neighbors<const N: usize>(&self, directions: [Direction; N]) -> [Coordinate; N] {
let mut result = [Coordinate::new(0.0, 0.0); N];
let mut idx = 0;
while idx < directions.len() {
result[idx] = self.neighbor(directions[idx]);
idx += 1;
}
result
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Bbox {
pub min: Coordinate,
pub max: Coordinate,
}
impl Bbox {
#[inline(always)]
pub const fn min(&self) -> &Coordinate {
&self.min
}
#[inline(always)]
pub const fn max(&self) -> &Coordinate {
&self.max
}
#[inline(always)]
pub const fn position(&self) -> GeoHashPosition {
let min = self.min;
let max = self.max;
GeoHashPosition {
coord: Coordinate {
latitude: (min.latitude + max.latitude) / 2.0,
longitude: (min.longitude + max.longitude) / 2.0,
},
lat_err: (max.latitude - min.latitude) / 2.0,
lon_err: (max.longitude - min.longitude) / 2.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Direction {
N,
NE,
E,
SE,
S,
SW,
W,
NW,
}
impl Direction {
pub const ALL: [Self; 8] = [Self::N, Self::NE, Self::E, Self::SE, Self::S, Self::SW, Self::W, Self::NW];
const fn to_lat_lon(self) -> (f64, f64) {
match self {
Direction::SW => (-1.0, -1.0),
Direction::S => (-1.0, 0.0),
Direction::SE => (-1.0, 1.0),
Direction::W => (0.0, -1.0),
Direction::E => (0.0, 1.0),
Direction::NW => (1.0, -1.0),
Direction::N => (1.0, 0.0),
Direction::NE => (1.0, 1.0),
}
}
}