use geo::Point;
use codearea::CodeArea;
use consts::{
SEPARATOR, SEPARATOR_POSITION, PADDING_CHAR, PADDING_CHAR_STR, CODE_ALPHABET, ENCODING_BASE,
LATITUDE_MAX, LONGITUDE_MAX, PAIR_CODE_LENGTH, PAIR_RESOLUTIONS, GRID_COLUMNS, GRID_ROWS,
MIN_TRIMMABLE_CODE_LEN,
};
use private::{
code_value, normalize_longitude, clip_latitude, compute_latitude_precision, prefix_by_reference,
narrow_region,
};
pub fn is_valid(_code: &str) -> bool {
let mut code: String = _code.to_string();
if code.len() < 3 {
return false;
}
if code.find(SEPARATOR).is_none() {
return false;
}
if code.find(SEPARATOR) != code.rfind(SEPARATOR) {
return false;
}
let spos = code.find(SEPARATOR).unwrap();
if spos % 2 == 1 || spos > SEPARATOR_POSITION {
return false;
}
if code.len() - spos - 1 == 1 {
return false;
}
let padstart = code.find(PADDING_CHAR);
if padstart.is_some() {
let ppos = padstart.unwrap();
if ppos == 0 || ppos % 2 == 1 {
return false;
}
if code.len() > spos + 1 {
return false;
}
let eppos = code.rfind(PADDING_CHAR).unwrap();
if eppos - ppos % 2 == 1 {
return false;
}
let padding: String = code.drain(ppos..eppos+1).collect();
if padding.chars().any(|c| c != PADDING_CHAR) {
return false;
}
}
code.chars()
.map(|c| c.to_ascii_uppercase())
.all(|c| c == SEPARATOR || CODE_ALPHABET.contains(&c))
}
pub fn is_short(_code: &str) -> bool {
is_valid(_code) &&
_code.find(SEPARATOR).unwrap() < SEPARATOR_POSITION
}
pub fn is_full(_code: &str) -> bool {
is_valid(_code) && !is_short(_code)
}
pub fn encode(pt: Point<f64>, code_length: usize) -> String {
let mut lat = clip_latitude(pt.lat());
let mut lng = normalize_longitude(pt.lng());
if lat > LATITUDE_MAX || (LATITUDE_MAX - lat) < 1e-10f64 {
lat -= compute_latitude_precision(code_length);
}
lat += LATITUDE_MAX;
lng += LONGITUDE_MAX;
let mut code = String::with_capacity(code_length + 1);
let mut digit = 0;
while digit < code_length {
narrow_region(digit, &mut lat, &mut lng);
let lat_digit = lat as usize;
let lng_digit = lng as usize;
if digit < PAIR_CODE_LENGTH {
code.push(CODE_ALPHABET[lat_digit]);
code.push(CODE_ALPHABET[lng_digit]);
digit += 2;
} else {
code.push(CODE_ALPHABET[4 * lat_digit + lng_digit]);
digit += 1;
}
lat -= lat_digit as f64;
lng -= lng_digit as f64;
if digit == SEPARATOR_POSITION {
code.push(SEPARATOR);
}
}
if digit < SEPARATOR_POSITION {
code.push_str(
PADDING_CHAR_STR.repeat(SEPARATOR_POSITION - digit).as_str()
);
code.push(SEPARATOR);
}
code
}
pub fn decode(_code: &str) -> Result<CodeArea, String> {
if !is_full(_code) {
return Err(format!("Code must be a valid full code: {}", _code));
}
let code = _code.to_string()
.replace(SEPARATOR, "")
.replace(PADDING_CHAR_STR, "")
.to_uppercase();
let mut lat = -LATITUDE_MAX;
let mut lng = -LONGITUDE_MAX;
let mut lat_res = ENCODING_BASE * ENCODING_BASE;
let mut lng_res = ENCODING_BASE * ENCODING_BASE;
for (idx, chr) in code.chars().enumerate() {
if idx < PAIR_CODE_LENGTH {
if idx % 2 == 0 {
lat_res /= ENCODING_BASE;
lat += lat_res * code_value(chr) as f64;
} else {
lng_res /= ENCODING_BASE;
lng += lng_res * code_value(chr) as f64;
}
} else {
lat_res /= GRID_ROWS;
lng_res /= GRID_COLUMNS;
lat += lat_res * (code_value(chr) as f64 / GRID_COLUMNS).trunc();
lng += lng_res * (code_value(chr) as f64 % GRID_COLUMNS);
}
}
Ok(CodeArea::new(lat, lng, lat + lat_res, lng + lng_res, code.len()))
}
pub fn shorten(_code: &str, ref_pt: Point<f64>) -> Result<String, String> {
if !is_full(_code) {
return Ok(_code.to_string());
}
if _code.find(PADDING_CHAR).is_some() {
return Err("Cannot shorten padded codes".to_owned());
}
let codearea: CodeArea = decode(_code).unwrap();
if codearea.code_length < MIN_TRIMMABLE_CODE_LEN {
return Err(format!("Code length must be at least {}", MIN_TRIMMABLE_CODE_LEN));
}
let range = (codearea.center.lat() - clip_latitude(ref_pt.lat())).abs().max(
(codearea.center.lng() - normalize_longitude(ref_pt.lng())).abs()
);
for i in 0..PAIR_RESOLUTIONS.len() - 2 {
let idx = PAIR_RESOLUTIONS.len() - 2 - i;
if range < (PAIR_RESOLUTIONS[idx] * 0.3f64) {
let mut code = _code.to_string();
code.drain(..((idx + 1) * 2));
return Ok(code);
}
}
Ok(_code.to_string())
}
pub fn recover_nearest(_code: &str, ref_pt: Point<f64>) -> Result<String, String> {
if !is_short(_code) {
if is_full(_code) {
return Ok(_code.to_string());
} else {
return Err(format!("Passed short code is not valid: {}", _code));
}
}
let prefix_len = SEPARATOR_POSITION - _code.find(SEPARATOR).unwrap();
let mut code = prefix_by_reference(ref_pt, prefix_len);
code.push_str(_code);
let code_area = decode(code.as_str()).unwrap();
let resolution = compute_latitude_precision(prefix_len);
let half_res = resolution / 2f64;
let mut latitude = code_area.center.lat();
let mut longitude = code_area.center.lng();
let ref_lat = clip_latitude(ref_pt.lat());
let ref_lng = normalize_longitude(ref_pt.lng());
if ref_lat + half_res < latitude && latitude - resolution >= -LATITUDE_MAX {
latitude -= resolution;
} else if ref_lat - half_res > latitude && latitude + resolution <= LATITUDE_MAX {
latitude += resolution;
}
if ref_lng + half_res < longitude {
longitude -= resolution;
} else if ref_lng - half_res > longitude {
longitude += resolution;
}
Ok(encode(Point::new(longitude, latitude), code_area.code_length))
}