use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(u8)]
pub enum GeoDistanceUnitType {
#[default]
M = 0,
Km,
Mi,
Ft,
}
pub struct GeoHash;
impl GeoHash {
pub const LONGITUDE_MIN: f64 = -180.0;
pub const LONGITUDE_MAX: f64 = 180.0;
pub const LATITUDE_MIN: f64 = -90.0;
pub const LATITUDE_MAX: f64 = 90.0;
pub const BITS_OF_PRECISION: u32 = 52;
pub const CODE_LENGTH: usize = 11;
pub fn geo_to_long_value(latitude: f64, longitude: f64) -> i64 {
if !(Self::LATITUDE_MIN..=Self::LATITUDE_MAX).contains(&latitude)
|| !(Self::LONGITUDE_MIN..=Self::LONGITUDE_MAX).contains(&longitude)
{
return -1;
}
const LAT_TO_UNIT_RANGE_RECIPROCAL: f64 = 1.0 / 180.0;
const LON_TO_UNIT_RANGE_RECIPROCAL: f64 = 1.0 / 360.0;
let lat_quantized = Self::quantize(latitude, LAT_TO_UNIT_RANGE_RECIPROCAL);
let lon_quantized = Self::quantize(longitude, LON_TO_UNIT_RANGE_RECIPROCAL);
let result = Self::morton_encode(lat_quantized, lon_quantized);
(result >> (u64::BITS - Self::BITS_OF_PRECISION)) as i64
}
#[inline]
pub fn quantize(value: f64, range_reciprocal: f64) -> u32 {
let y = (value.mul_add(range_reciprocal, 1.5).to_bits()) >> 20;
if y == (2.0f64.to_bits() >> 20) {
u32::MAX
} else {
y as u32
}
}
pub fn get_coordinates_from_long(hash: i64) -> (f64, f64) {
let full_hash = (hash as u64) << (u64::BITS - Self::BITS_OF_PRECISION);
let (lat_quantized, lon_quantized) = Self::morton_decode(full_hash);
let min_latitude = Self::dequantize(lat_quantized, Self::LATITUDE_MAX);
let min_longitude = Self::dequantize(lon_quantized, Self::LONGITUDE_MAX);
let (latitude_error, longitude_error) = Self::get_geo_error_by_precision();
(
min_latitude + latitude_error / 2.0,
min_longitude + longitude_error / 2.0,
)
}
#[inline]
pub fn dequantize(quantized_value: u32, range_max: f64) -> f64 {
let value = f64::from_bits(((quantized_value as u64) << 20) | (1023_u64 << 52));
(range_max + range_max).mul_add(value - 1.0, -range_max)
}
#[inline]
pub fn morton_encode(x: u32, y: u32) -> u64 {
Self::spread(x) | (Self::spread(y) << 1)
}
#[inline]
pub fn spread(x: u32) -> u64 {
let mut y = x as u64;
y = (y | (y << 16)) & 0x0000_FFFF_0000_FFFF;
y = (y | (y << 8)) & 0x00FF_00FF_00FF_00FF;
y = (y | (y << 4)) & 0x0F0F_0F0F_0F0F_0F0F;
y = (y | (y << 2)) & 0x3333_3333_3333_3333;
(y | (y << 1)) & 0x5555_5555_5555_5555
}
#[inline]
pub fn morton_decode(x: u64) -> (u32, u32) {
(Self::squash(x), Self::squash(x >> 1))
}
#[inline]
pub fn squash(x: u64) -> u32 {
let mut y = x & 0x5555_5555_5555_5555;
y = (y | (y >> 1)) & 0x3333_3333_3333_3333;
y = (y | (y >> 2)) & 0x0F0F_0F0F_0F0F_0F0F;
y = (y | (y >> 4)) & 0x00FF_00FF_00FF_00FF;
y = (y | (y >> 8)) & 0x0000_FFFF_0000_FFFF;
y = (y | (y >> 16)) & 0x0000_0000_FFFF_FFFF;
y as u32
}
pub fn get_geo_hash_code(hash: i64) -> [u8; Self::CODE_LENGTH] {
const BASE32_CHARS: &[u8; 32] = b"0123456789bcdefghjkmnpqrstuvwxyz";
let mut hash = hash;
let mut code = [0; Self::CODE_LENGTH];
code[Self::CODE_LENGTH - 1] = b'0';
for slot in code.iter_mut().take(Self::CODE_LENGTH - 1) {
*slot = BASE32_CHARS[((hash >> (Self::BITS_OF_PRECISION - 5)) & 0x1F) as usize];
hash <<= 5;
}
code
}
pub fn distance(source_lat: f64, source_lon: f64, target_lat: f64, target_lon: f64) -> f64 {
const EARTH_RADIUS_IN_METERS: f64 = 6_372_797.560_856;
let lon_radians = Self::degrees_to_radians(source_lon - target_lon);
let lon_haversine = (lon_radians / 2.0).sin().powi(2);
let lat_radians = Self::degrees_to_radians(source_lat - target_lat);
let lat_haversine = (lat_radians / 2.0).sin().powi(2);
let tmp =
Self::degrees_to_radians(source_lat).cos() * Self::degrees_to_radians(target_lat).cos();
2.0 * (lat_haversine + tmp * lon_haversine).sqrt().asin() * EARTH_RADIUS_IN_METERS
}
#[inline]
pub fn degrees_to_radians(degrees: f64) -> f64 {
degrees * PI / 180.0
}
pub fn is_point_within_radius(
radius: f64,
lat_center_point: f64,
lon_center_point: f64,
lat: f64,
lon: f64,
) -> Option<f64> {
let distance = Self::distance(lat_center_point, lon_center_point, lat, lon);
(distance < radius).then_some(distance)
}
pub fn get_distance_when_in_rectangle(
width_mts: f64,
height_mts: f64,
lat_center_point: f64,
lon_center_point: f64,
lat2: f64,
lon2: f64,
) -> Option<f64> {
let lon_distance = Self::distance(lat2, lon2, lat2, lon_center_point);
let lat_distance = Self::distance(lat2, lon2, lat_center_point, lon2);
if lon_distance > width_mts / 2.0 || lat_distance > height_mts / 2.0 {
return None;
}
Some(Self::distance(
lat_center_point,
lon_center_point,
lat2,
lon2,
))
}
#[inline]
pub fn get_geo_error_by_precision() -> (f64, f64) {
const LAT_BITS: i32 = GeoHash::BITS_OF_PRECISION as i32 / 2;
const LONG_BITS: i32 = GeoHash::BITS_OF_PRECISION as i32 - LAT_BITS;
let lat_error = 180.0 * 2.0_f64.powi(-LAT_BITS);
let long_error = 360.0 * 2.0_f64.powi(-LONG_BITS);
(lat_error, long_error)
}
#[inline]
pub fn convert_value_to_meters(value: f64, unit: GeoDistanceUnitType) -> f64 {
match unit {
GeoDistanceUnitType::Km => value / 0.001,
GeoDistanceUnitType::Ft => value / 3.280_84,
GeoDistanceUnitType::Mi => value / 0.000_621_371,
GeoDistanceUnitType::M => value,
}
}
#[inline]
pub fn convert_meters_to_units(value: f64, unit: GeoDistanceUnitType) -> f64 {
match unit {
GeoDistanceUnitType::Km => value * 0.001,
GeoDistanceUnitType::Ft => value * 3.280_84,
GeoDistanceUnitType::Mi => value * 0.000_621_371,
GeoDistanceUnitType::M => value,
}
}
}
#[cfg(test)]
mod tests {
use std::str::from_utf8;
use super::*;
#[test]
fn geo_hash_code_golden() {
let sf = GeoHash::geo_to_long_value(37.7749, -122.4194); assert_ne!(sf, -1);
let code_bytes = GeoHash::get_geo_hash_code(sf);
let code = from_utf8(&code_bytes).unwrap();
assert!(code.starts_with("9q8yy"), "{code}");
let tokyo = GeoHash::geo_to_long_value(35.6762, 139.6503);
let code_bytes = GeoHash::get_geo_hash_code(tokyo);
let code = from_utf8(&code_bytes).unwrap();
assert!(code.starts_with("xn76"), "{code}");
}
#[test]
fn round_trip_coordinates() {
let (lat_err, lon_err) = GeoHash::get_geo_error_by_precision();
for (lat, lon) in [
(0.0, 0.0),
(37.7749, -122.4194),
(-33.8688, 151.2093),
(89.9, 179.9),
(-45.0, -90.0),
] {
let hash = GeoHash::geo_to_long_value(lat, lon);
assert!(hash >= 0);
let (lat2, lon2) = GeoHash::get_coordinates_from_long(hash);
assert!((lat2 - lat).abs() <= lat_err, "{lat} vs {lat2}");
assert!((lon2 - lon).abs() <= lon_err, "{lon} vs {lon2}");
}
}
#[test]
fn out_of_range_coordinates() {
assert_eq!(GeoHash::geo_to_long_value(90.1, 0.0), -1);
assert_eq!(GeoHash::geo_to_long_value(-90.1, 0.0), -1);
assert_eq!(GeoHash::geo_to_long_value(0.0, 180.1), -1);
assert_eq!(GeoHash::geo_to_long_value(0.0, -180.1), -1);
assert_ne!(GeoHash::geo_to_long_value(90.0, 180.0), -1);
assert_ne!(GeoHash::geo_to_long_value(-90.0, -180.0), -1);
}
#[test]
fn morton_round_trip() {
for (x, y) in [(0u32, 0u32), (u32::MAX, 1), (0x1234_5678, 0x9abc_def0)] {
let m = GeoHash::morton_encode(x, y);
assert_eq!(GeoHash::morton_decode(m), (x, y));
}
assert_eq!(GeoHash::morton_encode(1, 0), 1);
assert_eq!(GeoHash::morton_encode(0, 1), 2);
}
#[test]
fn distance_golden() {
let d = GeoHash::distance(39.9042, 116.4074, 31.2304, 121.4737);
assert!((d - 1_067_000.0).abs() < 5_000.0, "{d}");
assert_eq!(GeoHash::distance(10.0, 20.0, 10.0, 20.0), 0.0);
}
#[test]
fn radius_and_rectangle() {
let within = GeoHash::is_point_within_radius(150_000.0, 0.0, 0.0, 1.0, 0.0);
assert!(within.is_some());
let outside = GeoHash::is_point_within_radius(100_000.0, 0.0, 0.0, 1.0, 0.0);
assert!(outside.is_none());
let inside = GeoHash::get_distance_when_in_rectangle(300_000.0, 300_000.0, 0.0, 0.0, 1.0, 0.0);
assert!(inside.is_some());
let outside = GeoHash::get_distance_when_in_rectangle(200_000.0, 200_000.0, 0.0, 0.0, 2.0, 0.0);
assert!(outside.is_none());
}
#[test]
fn unit_conversion() {
use GeoDistanceUnitType as U;
assert_eq!(GeoHash::convert_value_to_meters(1.0, U::Km), 1000.0);
assert!((GeoHash::convert_value_to_meters(1.0, U::Mi) - 1609.344).abs() < 0.01);
assert!((GeoHash::convert_value_to_meters(1.0, U::Ft) - 0.3048).abs() < 1e-6);
assert_eq!(GeoHash::convert_meters_to_units(1000.0, U::Km), 1.0);
assert_eq!(GeoHash::convert_value_to_meters(5.0, U::M), 5.0);
}
}