use core::f64::consts::PI;
const R: f64 = 6_378_137.0;
pub const MAX_LAT: f64 = 85.051_128_78;
pub const DIGIPIN_GRID: [[char; 6]; 6] = [
['I', 'A', 'B', 'C', 'D', 'E'],
['G', 'H', 'J', 'K', 'L', 'M'],
['N', 'P', 'Q', 'R', 'S', 'T'],
['U', 'r', 'W', 'X', 'Y', 'Z'],
['a', 'b', '9', 'd', 'V', 'F'],
['2', '3', '4', '5', '6', '7'],
];
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LatLng {
pub latitude: f64,
pub longitude: f64,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("lat/lon must be finite numbers")]
NonFiniteInput,
#[error("invalid DIGIPIN")]
InvalidPin,
#[error("invalid character '{0}' in DIGIPIN")]
InvalidChar(char),
}
#[inline]
fn clamp(v: f64, lo: f64, hi: f64) -> f64 { v.max(lo).min(hi) }
#[inline]
fn normalize_lon(lon: f64) -> f64 {
let x = ((lon + 180.0) % 360.0 + 360.0) % 360.0 - 180.0;
if x == 180.0 { -180.0 } else { x }
}
#[inline] fn lon_to_x(lon_deg: f64) -> f64 { R * normalize_lon(lon_deg).to_radians() }
#[inline] fn x_to_lon(x: f64) -> f64 { normalize_lon((x / R).to_degrees()) }
#[inline]
fn lat_to_y(lat_deg: f64) -> f64 {
let phi = clamp(lat_deg, -MAX_LAT, MAX_LAT).to_radians();
R * f64::ln(f64::tan(PI / 4.0 + phi / 2.0))
}
#[inline]
fn y_to_lat(y: f64) -> f64 {
let phi = 2.0 * f64::atan(f64::exp(y / R)) - PI / 2.0;
phi.to_degrees()
}
struct MercBounds { min_x: f64, max_x: f64, min_y: f64, max_y: f64 }
fn merc_bounds() -> MercBounds {
let min_x = -PI * R;
let max_x = PI * R;
let max_phi = MAX_LAT.to_radians() / 2.0 + PI / 4.0;
let max_y = R * f64::ln(f64::tan(max_phi));
let min_y = -max_y;
MercBounds { min_x, max_x, min_y, max_y }
}
pub fn get_digi_pin(lat: f64, lon: f64, levels: usize) -> Result<String, Error> {
if !lat.is_finite() || !lon.is_finite() { return Err(Error::NonFiniteInput); }
let lat = clamp(lat, -MAX_LAT, MAX_LAT);
let lon = normalize_lon(lon);
let mut x = lon_to_x(lon);
let mut y = lat_to_y(lat);
let eps = 1e-9_f64;
let MercBounds { mut min_x, mut max_x, mut min_y, mut max_y } = merc_bounds();
x = x.max(min_x + eps).min(max_x - eps);
y = y.max(min_y + eps).min(max_y - eps);
let mut code = String::with_capacity(levels);
for _ in 0..levels {
let x_div = (max_x - min_x) / 6.0;
let y_div = (max_y - min_y) / 6.0;
let row_raw = 5.0 - ((y - min_y) / y_div).floor();
let col_raw = ((x - min_x) / x_div).floor();
let row = clamp(row_raw, 0.0, 5.0) as usize;
let col = clamp(col_raw, 0.0, 5.0) as usize;
code.push(DIGIPIN_GRID[row][col]);
let new_max_y = min_y + y_div * (6.0 - row as f64);
let new_min_y = min_y + y_div * (5.0 - row as f64);
min_x = min_x + x_div * (col as f64);
let new_max_x = min_x + x_div;
min_y = new_min_y; max_y = new_max_y; max_x = new_max_x;
}
Ok(group_code(&code))
}
pub fn get_lat_lng_from_digipin(digipin: &str) -> Result<LatLng, Error> {
let pin: String = digipin.chars().filter(|&ch| ch != '-').collect();
if pin.is_empty() { return Err(Error::InvalidPin); }
let MercBounds { mut min_x, mut max_x, mut min_y, mut max_y } = merc_bounds();
for ch in pin.chars() {
let (ri, ci) = lookup_grid(ch).ok_or(Error::InvalidChar(ch))?;
let x_div = (max_x - min_x) / 6.0;
let y_div = (max_y - min_y) / 6.0;
let y1 = max_y - y_div * ((ri + 1) as f64);
let y2 = max_y - y_div * (ri as f64);
let x1 = min_x + x_div * (ci as f64);
let x2 = x1 + x_div;
min_y = y1; max_y = y2;
min_x = x1; max_x = x2;
}
let cx = (min_x + max_x) / 2.0;
let cy = (min_y + max_y) / 2.0;
Ok(LatLng { latitude: y_to_lat(cy), longitude: x_to_lon(cx) })
}
pub fn approx_cell_size_meters(levels: usize) -> f64 {
let world = 2.0 * PI * R;
world / 6f64.powi(levels as i32)
}
fn group_code(raw: &str) -> String {
if raw.len() == 10 {
format!("{}-{}-{}", &raw[0..4], &raw[4..8], &raw[8..10])
} else {
let mut out = String::with_capacity(raw.len() + raw.len()/4);
for (i, ch) in raw.chars().enumerate() {
if i > 0 && i % 4 == 0 { out.push('-'); }
out.push(ch);
}
out
}
}
#[inline]
fn lookup_grid(ch: char) -> Option<(usize, usize)> {
for (r, row) in DIGIPIN_GRID.iter().enumerate() {
for (c, &sym) in row.iter().enumerate() {
if sym == ch { return Some((r, c)); }
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lon_normalization() {
assert_eq!(super::normalize_lon(180.0), -180.0);
assert!((super::normalize_lon(181.0) - -179.0).abs() < 1e-12);
assert!((super::normalize_lon(-181.0) - 179.0).abs() < 1e-12);
}
#[test]
fn approx_size_levels() {
let l5 = approx_cell_size_meters(5);
let l6 = approx_cell_size_meters(6);
assert!(l6 < l5);
}
#[test]
fn roundtrip_delhi_like() {
let lat = 28.6139_f64;
let lon = 77.2090_f64;
let code = get_digi_pin(lat, lon, 10).unwrap();
let ll = get_lat_lng_from_digipin(&code).unwrap();
let dist_lat = (ll.latitude - lat).abs();
let dist_lon = (ll.longitude - lon).abs();
assert!(dist_lat < 0.05);
assert!(dist_lon < 0.05);
}
#[test]
fn grouping_rules() {
assert_eq!(super::group_code("ABCDEFGHIJ"), "ABCD-EFGH-IJ");
assert_eq!(super::group_code("ABCDEFGH"), "ABCD-EFGH");
assert_eq!(super::group_code("ABCDEF"), "ABCD-EF");
assert_eq!(super::group_code("ABCD"), "ABCD");
assert_eq!(super::group_code("ABC"), "ABC");
}
#[test]
fn invalid_char() {
let bad = "AAAA$";
let err = get_lat_lng_from_digipin(bad).unwrap_err();
match err {
Error::InvalidChar('$') => {}
_ => panic!("unexpected error"),
}
}
}