use std::{fmt, fmt::Display, str::FromStr};
use crate::{Coordinate, CoordinateError};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Geohash {
bounding_top_left: Coordinate,
bounding_bottom_right: Coordinate,
}
impl Geohash {
pub fn center(&self) -> Coordinate {
Coordinate {
lat: (self.bounding_top_left.lat + self.bounding_bottom_right.lat) / 2.,
lng: (self.bounding_top_left.lng + self.bounding_bottom_right.lng) / 2.,
}
}
pub fn height(&self) -> f64 {
self.bounding_bottom_right.lat - self.bounding_top_left.lat
}
pub fn width(&self) -> f64 {
self.bounding_bottom_right.lng - self.bounding_top_left.lng
}
pub fn hash_with_precision(&self, total_bits: usize) -> Result<String, CoordinateError> {
let lat_bits = total_bits / 2;
let lng_bits = total_bits / 2 + total_bits % 2;
let bits_fit_in_char = total_bits % 5 == 0;
let bits_correct_ratio = if total_bits % 10 == 0 {
lat_bits == lng_bits
} else {
lat_bits == lng_bits - 1
};
if bits_fit_in_char && bits_correct_ratio {
let cells_n_lat = 1usize << lat_bits;
let cells_n_lng = 1usize << lng_bits;
let lat = (90. + self.center().lat) / 180. * cells_n_lat as f64;
let lng = (180. + self.center().lng) / 360. * cells_n_lng as f64;
let lat = lat.floor() as usize;
let lng = lng.floor() as usize;
let lat_bits = (0..lat_bits).rev().map(|i| Some((lat >> i) & 0b1));
let lng_bits = (0..lng_bits).rev().map(|i| Some((lng >> i) & 0b1));
let all_bits: Vec<usize> = lng_bits
.zip(lat_bits.chain(std::iter::repeat(None)))
.flat_map(|(a, b)| [a, b])
.map_while(|item| item)
.collect();
let mut res = "".to_string();
for chunk in all_bits.chunks(5) {
let mut byte = 0;
for (i, value) in chunk.iter().enumerate() {
byte |= value << (4 - i);
}
res.push(char::try_from(GeohashB32(byte as u8)).unwrap());
}
Ok(res)
} else {
Err(CoordinateError::Malformed)
}
}
pub fn hash_with_max_length(&self, length: usize) -> String {
self.hash_with_precision(length * 5).unwrap()
}
pub fn get_inner_hash(&self) -> String {
let needed_bits = if self.height() == 0.0 || self.width() == 0.0 {
8 * 5
} else {
let lat_bits = (360. / self.height()).ceil() as usize
- if self.crosses_vertical_chunks() { 1 } else { 0 };
let lng_bits = (360. / self.width()).ceil() as usize
- if self.crosses_horizontal_chunks() {
1
} else {
0
};
let min_bits = lat_bits.max(lng_bits);
(((min_bits - 1) / 5) + 1) * 5
};
self.hash_with_precision(needed_bits).unwrap()
}
pub fn get_outer_hash(&self) -> String {
let needed_bits = if self.height() == 0.0 || self.width() == 0.0 {
8 * 5
} else {
let lat_bits = (360. / self.height()).floor() as usize
- if self.crosses_vertical_chunks() { 1 } else { 0 };
let lng_bits = (360. / self.width()).floor() as usize
- if self.crosses_horizontal_chunks() {
1
} else {
0
};
let max_bits = lat_bits.min(lng_bits);
(((max_bits + 1) / 5) - 1) * 5
};
self.hash_with_precision(needed_bits).unwrap()
}
fn crosses_horizontal_chunks(&self) -> bool {
let left_cell = (self.bounding_top_left.lng / self.width()).floor() as usize;
let right_cell = (self.bounding_bottom_right.lng / self.width()).floor() as usize;
left_cell == right_cell
}
fn crosses_vertical_chunks(&self) -> bool {
let top_cell = (self.bounding_top_left.lat / self.height()).floor() as usize;
let bottom_cell = (self.bounding_bottom_right.lat / self.height()).floor() as usize;
top_cell == bottom_cell
}
}
impl Default for Geohash {
fn default() -> Self {
Self {
bounding_top_left: Coordinate {
lat: 90.,
lng: -180.,
},
bounding_bottom_right: Coordinate {
lat: -90.,
lng: 180.,
},
}
}
}
impl From<Coordinate> for Geohash {
fn from(coord: Coordinate) -> Self {
Geohash {
bounding_top_left: coord.clone(),
bounding_bottom_right: coord,
}
}
}
impl From<Geohash> for Coordinate {
fn from(hash: Geohash) -> Self {
hash.center()
}
}
impl FromStr for Geohash {
type Err = CoordinateError;
fn from_str(str_hash: &str) -> Result<Self, Self::Err> {
let b32s = str_hash.chars().map(GeohashB32::try_from);
let first_bits_lat = [1, 0].iter().cycle();
b32s.zip(first_bits_lat)
.try_fold(Geohash::default(), |acc, (b32, first_bit_lat)| {
b32.map(|b32| {
let mut res = acc.clone();
for i in (0..=4).rev() {
let bit = (b32.0 >> i) & 0b1;
if (i + first_bit_lat) % 2 == 0 {
let mid_lat =
(res.bounding_top_left.lat + res.bounding_bottom_right.lat) / 2.;
if bit == 0 {
res.bounding_top_left.lat = mid_lat;
} else {
res.bounding_bottom_right.lat = mid_lat;
}
} else {
let mid_lng =
(res.bounding_top_left.lng + res.bounding_bottom_right.lng) / 2.;
if bit == 0 {
res.bounding_bottom_right.lng = mid_lng;
} else {
res.bounding_top_left.lng = mid_lng;
}
}
}
res
})
})
}
}
impl Display for Geohash {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.get_outer_hash())
}
}
#[derive(Debug, PartialEq, Eq)]
struct GeohashB32(u8);
impl TryFrom<char> for GeohashB32 {
type Error = CoordinateError;
fn try_from(c: char) -> Result<Self, Self::Error> {
let c = c.to_ascii_lowercase() as u8;
Ok(GeohashB32(match c {
b'0'..=b'9' => c - b'0',
b'a' => return Err(Self::Error::InvalidValue),
b'b'..=b'h' => c - b'b' + 10,
b'i' => return Err(Self::Error::InvalidValue),
b'j' | b'k' => c - b'j' + 17,
b'l' => return Err(Self::Error::InvalidValue),
b'm' | b'n' => c - b'm' + 19,
b'o' => return Err(Self::Error::InvalidValue),
b'p'..=b'z' => c - b'p' + 21,
_ => return Err(Self::Error::InvalidValue),
}))
}
}
impl TryFrom<GeohashB32> for char {
type Error = CoordinateError;
fn try_from(ghb: GeohashB32) -> Result<char, Self::Error> {
Ok(match ghb.0 {
0..=9 => char::from_digit(ghb.0 as u32, 10).unwrap(),
10..=16 => (b'b' + ghb.0 - 10) as char,
17..=18 => (b'j' + ghb.0 - 17) as char,
19..=20 => (b'm' + ghb.0 - 19) as char,
21..=32 => (b'p' + ghb.0 - 21) as char,
_ => return Err(Self::Error::InvalidValue),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const ALPHABET: &str = "0123456789bcdefghjkmnpqrstuvwxyz";
#[test]
fn test_char_to_geohashb32() {
for (i, expected) in ALPHABET.chars().enumerate() {
assert_eq!(GeohashB32::try_from(expected).unwrap(), GeohashB32(i as u8));
}
}
#[test]
fn test_char_to_geohashb32_errors() {
assert!(GeohashB32::try_from('a').is_err());
assert!(GeohashB32::try_from('ö').is_err());
assert!(GeohashB32::try_from('💥').is_err());
}
#[test]
fn test_geohashb32_to_char() {
for (i, expected) in ALPHABET.chars().enumerate() {
assert_eq!(char::try_from(GeohashB32(i as u8)).unwrap(), expected);
}
}
#[test]
fn test_geohash_decode_encode() {
for expected in ALPHABET.chars() {
let geohash = Geohash::from_str(&expected.to_string());
assert!(geohash.is_ok());
let geohash = geohash.unwrap();
let result = geohash.hash_with_max_length(1);
assert_eq!(result, expected.to_string());
}
}
#[test]
fn test_geohash_cases() {
let cases = vec![Coordinate::from_str("50.944133,7.573788").unwrap()];
for case in cases {
let geohash = Geohash::try_from(case);
assert!(geohash.is_ok());
let geohash = geohash.unwrap();
let result = geohash.get_outer_hash();
println!("result {result}");
assert!(false)
}
}
#[test]
fn test_geohash_decode_encode_stresstest() {
let hashes = build_test_hash_with_length(2, None);
println!("hi {}", hashes.len());
for hash in hashes {
println!("Testing hash {hash}");
let geohash = Geohash::from_str(&hash);
assert!(geohash.is_ok());
let geohash = geohash.unwrap();
let result = geohash.hash_with_max_length(hash.chars().count());
assert_eq!(result, hash);
}
}
fn build_test_hash_with_length(length: usize, per_depth: Option<usize>) -> Vec<String> {
match length {
0 => vec![],
1 => ALPHABET.chars().map(|c| c.to_string()).collect(),
_ => {
let sub_hashes = build_test_hash_with_length(length - 1, per_depth);
let mut res = vec![];
for possible in ALPHABET.chars().take(per_depth.unwrap_or(32)) {
res.push(possible.to_string());
for sub_hash in sub_hashes.iter() {
let mut res_str = possible.to_string();
res_str.push_str(&sub_hash);
res.push(res_str);
}
}
res
}
}
}
}