use crate::{LngLat, PolylineError, PolylineResult};
fn validate_coordinate(coord: &LngLat) -> PolylineResult<()> {
if !coord.lng_deg.is_finite() || !coord.lat_deg.is_finite() {
return Err(PolylineError::InvalidCoordinate(
"NaN or infinity coordinate".to_string(),
));
}
if coord.lng_deg < -180.0 || coord.lng_deg > 180.0 {
return Err(PolylineError::InvalidCoordinate(format!(
"Longitude {} out of bounds [-180, 180]",
coord.lng_deg
)));
}
if coord.lat_deg < -90.0 || coord.lat_deg > 90.0 {
return Err(PolylineError::InvalidCoordinate(format!(
"Latitude {} out of bounds [-90, 90]",
coord.lat_deg
)));
}
Ok(())
}
fn safe_coordinate_to_int(coord_deg: f64, factor: f64) -> PolylineResult<i64> {
let scaled = coord_deg * factor;
if scaled > (i64::MAX as f64) || scaled < (i64::MIN as f64) || !scaled.is_finite() {
return Err(PolylineError::CoordinateOverflow);
}
Ok(scaled.round() as i64)
}
pub fn encode(coordinates: &[LngLat], precision: u8) -> PolylineResult<String> {
if precision == 0 || precision > 11 {
return Err(PolylineError::InvalidPrecision(precision));
}
if coordinates.is_empty() {
return Ok(String::new());
}
let factor = 10_i64.pow(precision as u32) as f64;
let estimated_capacity = coordinates.len() * 8 + 16; let mut result = String::with_capacity(estimated_capacity);
let mut prev_lat = 0i64;
let mut prev_lng = 0i64;
for coord in coordinates {
validate_coordinate(coord)?;
let lat = safe_coordinate_to_int(coord.lat_deg, factor)?;
let lng = safe_coordinate_to_int(coord.lng_deg, factor)?;
let delta_lat = lat
.checked_sub(prev_lat)
.ok_or(PolylineError::CoordinateOverflow)?;
let delta_lng = lng
.checked_sub(prev_lng)
.ok_or(PolylineError::CoordinateOverflow)?;
encode_signed_number(delta_lat, &mut result);
encode_signed_number(delta_lng, &mut result);
prev_lat = lat;
prev_lng = lng;
}
Ok(result)
}
fn encode_signed_number(value: i64, result: &mut String) {
let mut value = value << 1;
if value < 0 {
value = !value;
}
let mut bytes = Vec::with_capacity(12);
while value >= 0x20 {
bytes.push((0x20 | (value & 0x1f)) as u8 + 63);
value >>= 5;
}
bytes.push((value as u8) + 63);
let encoded_str = unsafe { std::str::from_utf8_unchecked(&bytes) };
result.push_str(encoded_str);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_empty() {
let coords: Vec<LngLat> = vec![];
assert_eq!(encode(&coords, 5).unwrap(), "");
}
#[test]
fn test_encode_single_point() {
let coords = vec![LngLat::new_deg(-122.4194, 37.7749)];
let encoded = encode(&coords, 5).unwrap();
assert!(!encoded.is_empty());
}
#[test]
fn test_encode_google_example() {
let coords = vec![
LngLat::new_deg(-120.2, 38.5),
LngLat::new_deg(-120.95, 40.7),
LngLat::new_deg(-126.453, 43.252),
];
let encoded = encode(&coords, 5).unwrap();
assert_eq!(encoded, "_p~iF~ps|U_ulLnnqC_mqNvxq`@");
}
#[test]
fn test_encode_precision_validation() {
let coords = vec![LngLat::new_deg(0.0, 0.0)];
assert!(encode(&coords, 0).is_err());
assert!(encode(&coords, 12).is_err());
assert!(encode(&coords, 5).is_ok());
assert!(encode(&coords, 6).is_ok());
}
#[test]
fn test_encode_negative_coordinates() {
let coords = vec![LngLat::new_deg(-1.0, -1.0), LngLat::new_deg(-2.0, -2.0)];
let encoded = encode(&coords, 5).unwrap();
assert!(!encoded.is_empty());
}
#[test]
fn test_encode_coordinate_validation() {
let nan_coord = vec![LngLat::new_deg(f64::NAN, 0.0)];
assert!(matches!(
encode(&nan_coord, 5),
Err(PolylineError::InvalidCoordinate(_))
));
let nan_coord = vec![LngLat::new_deg(0.0, f64::NAN)];
assert!(matches!(
encode(&nan_coord, 5),
Err(PolylineError::InvalidCoordinate(_))
));
let inf_coord = vec![LngLat::new_deg(f64::INFINITY, 0.0)];
assert!(matches!(
encode(&inf_coord, 5),
Err(PolylineError::InvalidCoordinate(_))
));
let neg_inf_coord = vec![LngLat::new_deg(0.0, f64::NEG_INFINITY)];
assert!(matches!(
encode(&neg_inf_coord, 5),
Err(PolylineError::InvalidCoordinate(_))
));
}
#[test]
fn test_encode_coordinate_bounds() {
let invalid_lng_high = vec![LngLat::new_deg(180.1, 0.0)];
assert!(matches!(
encode(&invalid_lng_high, 5),
Err(PolylineError::InvalidCoordinate(_))
));
let invalid_lng_low = vec![LngLat::new_deg(-180.1, 0.0)];
assert!(matches!(
encode(&invalid_lng_low, 5),
Err(PolylineError::InvalidCoordinate(_))
));
let invalid_lat_high = vec![LngLat::new_deg(0.0, 90.1)];
assert!(matches!(
encode(&invalid_lat_high, 5),
Err(PolylineError::InvalidCoordinate(_))
));
let invalid_lat_low = vec![LngLat::new_deg(0.0, -90.1)];
assert!(matches!(
encode(&invalid_lat_low, 5),
Err(PolylineError::InvalidCoordinate(_))
));
let valid_bounds = vec![LngLat::new_deg(-180.0, -90.0), LngLat::new_deg(180.0, 90.0)];
assert!(encode(&valid_bounds, 5).is_ok());
}
#[test]
fn test_encode_overflow_protection() {
let invalid_lng = vec![LngLat::new_deg(1000.0, 0.0)];
assert!(matches!(
encode(&invalid_lng, 5),
Err(PolylineError::InvalidCoordinate(_))
));
let coords = vec![
LngLat::new_deg(0.0, 0.0),
LngLat::new_deg(180.0, 90.0), ];
assert!(encode(&coords, 5).is_ok());
let high_precision_coords = vec![LngLat::new_deg(179.9999999, 89.9999999)];
assert!(encode(&high_precision_coords, 11).is_ok());
}
}