From 961d122a641f511890de37dbc644cb21a4474ba9 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Sun, 27 Oct 2024 10:33:07 +0900 Subject: [PATCH] First whack at being generic over CoordFloat --- Cargo.toml | 2 +- src/errors.rs | 25 +++++--- src/lib.rs | 165 +++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 147 insertions(+), 45 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 523c6a2..c9e69b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "polyline" description = "Encoder and decoder for the Google Encoded Polyline format" -version = "0.11.0" +version = "0.12.0" repository = "https://github.com/georust/polyline" documentation = "https://docs.rs/polyline/" readme = "README.md" diff --git a/src/errors.rs b/src/errors.rs index 5c68eeb..8efd47f 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,19 +1,20 @@ //! Errors that can occur during encoding / decoding of Polylines -use geo_types::Coord; +use std::any::type_name; +use geo_types::{Coord, CoordFloat}; #[derive(Debug, PartialEq)] #[non_exhaustive] -pub enum PolylineError { +pub enum PolylineError { LongitudeCoordError { /// The coordinate value that caused the error due to being outside the range `-180.0..180.0` - coord: f64, + coord: T, /// The string index of the coordinate error idx: usize, }, LatitudeCoordError { /// The coordinate value that caused the error due to being outside the range `-90.0..90.0` - coord: f64, + coord: T, /// The string index of the coordinate error idx: usize, }, @@ -27,21 +28,24 @@ pub enum PolylineError { }, EncodeToCharError, CoordEncodingError { - coord: Coord, + coord: Coord, /// The array index of the coordinate error idx: usize, }, + /// Unable to convert a value to the desired type + // TODO: Decide what info we want to express here + NumericCastFailure } -impl std::error::Error for PolylineError {} -impl std::fmt::Display for PolylineError { +impl std::error::Error for PolylineError {} +impl std::fmt::Display for PolylineError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { PolylineError::LongitudeCoordError { coord, idx } => { - write!(f, "longitude out of bounds: {} at position {}", coord, idx) + write!(f, "longitude out of bounds: {:?} at position {}", coord, idx) } PolylineError::LatitudeCoordError { coord, idx } => { - write!(f, "latitude out of bounds: {} at position {}", coord, idx) + write!(f, "latitude out of bounds: {:?} at position {}", coord, idx) } PolylineError::DecodeError { idx } => { write!(f, "cannot decode character at index {}", idx) @@ -56,6 +60,9 @@ impl std::fmt::Display for PolylineError { "the coordinate {:?} at index: {} could not be encoded", coord, idx ) + }, + PolylineError::NumericCastFailure => { + write!(f, "number is not representable as type {}", type_name::()) } } } diff --git a/src/lib.rs b/src/lib.rs index 506a582..8ba5276 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ pub mod errors; use errors::PolylineError; -use geo_types::{Coord, LineString}; +use geo_types::{Coord, CoordFloat, LineString}; use std::char; use std::iter::{Enumerate, Peekable}; @@ -34,13 +34,13 @@ const MAX_LONGITUDE: f64 = 180.0; const MIN_LATITUDE: f64 = -90.0; const MAX_LATITUDE: f64 = 90.0; -fn scale(n: f64, factor: i32) -> i64 { - let scaled = n * (f64::from(factor)); - scaled.round() as i64 +fn scale(n: T, factor: T) -> Result> { + let scaled = n * factor; + scaled.round().to_i64().ok_or(PolylineError::NumericCastFailure) } #[inline(always)] -fn encode(delta: i64, output: &mut String) -> Result<(), PolylineError> { +fn encode(delta: i64, output: &mut String) -> Result<(), PolylineError> { let mut value = delta << 1; if value < 0 { value = !value; @@ -67,24 +67,26 @@ fn encode(delta: i64, output: &mut String) -> Result<(), PolylineError> { /// let coords = line_string![(x: 2.0, y: 1.0), (x: 4.0, y: 3.0)]; /// let encoded_vec = polyline::encode_coordinates(coords, 5).unwrap(); /// ``` -pub fn encode_coordinates(coordinates: C, precision: u32) -> Result +pub fn encode_coordinates(coordinates: C, precision: u32) -> Result> where - C: IntoIterator>, + C: IntoIterator>, { let base: i32 = 10; - let factor: i32 = base.pow(precision); + let Some(factor) = T::from(base.pow(precision)) else { + return Err(PolylineError::NumericCastFailure) + }; let mut output = String::new(); let mut previous = Coord { x: 0, y: 0 }; for (i, next) in coordinates.into_iter().enumerate() { - if !(MIN_LATITUDE..=MAX_LATITUDE).contains(&next.y) { + if !(T::from(MIN_LATITUDE)..=T::from(MAX_LATITUDE)).contains(&next.y.into()) { return Err(PolylineError::LatitudeCoordError { coord: next.y, idx: i, }); } - if !(MIN_LONGITUDE..=MAX_LONGITUDE).contains(&next.x) { + if !(T::from(MIN_LONGITUDE)..=T::from(MAX_LONGITUDE)).contains(&next.x.into()) { return Err(PolylineError::LongitudeCoordError { coord: next.x, idx: i, @@ -92,16 +94,16 @@ where } let scaled_next = Coord { - x: scale(next.x, factor), - y: scale(next.y, factor), + x: scale(next.x, factor)?, + y: scale(next.y, factor)?, }; - encode(scaled_next.y - previous.y, &mut output).map_err(|_| { + encode(scaled_next.y - previous.y, &mut output).map_err(|_: PolylineError| { PolylineError::CoordEncodingError { coord: next, idx: i, } })?; - encode(scaled_next.x - previous.x, &mut output).map_err(|_| { + encode(scaled_next.x - previous.x, &mut output).map_err(|_: PolylineError| { PolylineError::CoordEncodingError { coord: next, idx: i, @@ -121,22 +123,24 @@ where /// ``` /// use polyline; /// -/// let decoded_polyline = polyline::decode_polyline(&"_p~iF~ps|U_ulLnnqC_mqNvxq`@", 5); +/// let decoded_polyline = polyline::decode_polyline::(&"_p~iF~ps|U_ulLnnqC_mqNvxq`@", 5); /// ``` -pub fn decode_polyline(polyline: &str, precision: u32) -> Result, PolylineError> { +pub fn decode_polyline(polyline: &str, precision: u32) -> Result, PolylineError> { let mut scaled_lat: i64 = 0; let mut scaled_lon: i64 = 0; let mut coordinates = vec![]; let base: i32 = 10; - let factor = i64::from(base.pow(precision)); + let Some(factor) = T::from(base.pow(precision)) else { + return Err(PolylineError::NumericCastFailure) + }; let mut chars = polyline.as_bytes().iter().copied().enumerate().peekable(); while let Some((lat_start, _)) = chars.peek().copied() { let latitude_change = decode_next(&mut chars)?; scaled_lat += latitude_change; - let lat = scaled_lat as f64 / factor as f64; - if !(MIN_LATITUDE..=MAX_LATITUDE).contains(&lat) { + let lat = T::from(scaled_lat).ok_or(PolylineError::NumericCastFailure)? / factor; + if !(MIN_LATITUDE..=MAX_LATITUDE).contains(&lat.to_f64().ok_or(PolylineError::NumericCastFailure)?) { return Err(PolylineError::LatitudeCoordError { coord: lat, idx: lat_start, @@ -148,8 +152,8 @@ pub fn decode_polyline(polyline: &str, precision: u32) -> Result }; let longitude_change = decode_next(&mut chars)?; scaled_lon += longitude_change; - let lon = scaled_lon as f64 / factor as f64; - if !(MIN_LONGITUDE..=MAX_LONGITUDE).contains(&lon) { + let lon = T::from(scaled_lon).ok_or(PolylineError::NumericCastFailure)? / factor; + if !(MIN_LONGITUDE..=MAX_LONGITUDE).contains(&lon.to_f64().ok_or(PolylineError::NumericCastFailure)?) { return Err(PolylineError::LongitudeCoordError { coord: lon, idx: lon_start, @@ -162,9 +166,9 @@ pub fn decode_polyline(polyline: &str, precision: u32) -> Result Ok(LineString::new(coordinates)) } -fn decode_next( - chars: &mut Peekable>>, -) -> Result { +fn decode_next( + chars: &mut Peekable>>, +) -> Result> { let mut shift = 0; let mut result = 0; for (idx, mut byte) in chars.by_ref() { @@ -260,9 +264,9 @@ mod tests { } #[test] - fn broken_string() { + fn broken_string_f64() { let s = "_p~iF~ps|U_u🗑lLnnqC_mqNvxq`@"; - let err = decode_polyline(s, 5).unwrap_err(); + let err = decode_polyline::(s, 5).unwrap_err(); match err { crate::errors::PolylineError::LatitudeCoordError { coord, idx } => { assert_eq!(coord, 2306360.53104); @@ -273,9 +277,32 @@ mod tests { } #[test] - fn invalid_string() { + fn broken_string_f32() { + let s = "_p~iF~ps|U_u🗑lLnnqC_mqNvxq`@"; + let err = decode_polyline::(s, 5).unwrap_err(); + match err { + crate::errors::PolylineError::LatitudeCoordError { coord, idx } => { + assert_eq!(coord, 2306360.53104); + assert_eq!(idx, 10); + } + _ => panic!("Got wrong error"), + } + } + + #[test] + fn invalid_string_f64() { + let s = "invalid_polyline_that_should_be_handled_gracefully"; + let err = decode_polyline::(s, 5).unwrap_err(); + match err { + crate::errors::PolylineError::DecodeError { idx } => assert_eq!(idx, 12), + _ => panic!("Got wrong error"), + } + } + + #[test] + fn invalid_string_f32() { let s = "invalid_polyline_that_should_be_handled_gracefully"; - let err = decode_polyline(s, 5).unwrap_err(); + let err = decode_polyline::(s, 5).unwrap_err(); match err { crate::errors::PolylineError::DecodeError { idx } => assert_eq!(idx, 12), _ => panic!("Got wrong error"), @@ -283,9 +310,9 @@ mod tests { } #[test] - fn another_invalid_string() { + fn another_invalid_string_f64() { let s = "ugh_ugh"; - let err = decode_polyline(s, 5).unwrap_err(); + let err = decode_polyline::(s, 5).unwrap_err(); match err { crate::errors::PolylineError::LatitudeCoordError { coord, idx } => { assert_eq!(coord, 49775.95019); @@ -296,7 +323,20 @@ mod tests { } #[test] - fn bad_coords() { + fn another_invalid_string_f32() { + let s = "ugh_ugh"; + let err = decode_polyline::(s, 5).unwrap_err(); + match err { + crate::errors::PolylineError::LatitudeCoordError { coord, idx } => { + assert_eq!(coord, 49775.95019); + assert_eq!(idx, 0); + } + _ => panic!("Got wrong error"), + } + } + + #[test] + fn bad_coords_f64() { // Can't have a latitude > 90.0 let res: LineString = vec![[-120.2, 38.5], [-120.95, 40.7], [-126.453, 430.252]].into(); @@ -311,8 +351,23 @@ mod tests { } #[test] - fn should_not_trigger_overflow() { - decode_polyline( + fn bad_coords_f32() { + // Can't have a latitude > 90.0 + let res: LineString = + vec![[-120.2, 38.5], [-120.95, 40.7], [-126.453, 430.252]].into(); + let err = encode_coordinates(res, 5).unwrap_err(); + match err { + crate::errors::PolylineError::LatitudeCoordError { coord, idx } => { + assert_eq!(coord, 430.252); + assert_eq!(idx, 2); + } + _ => panic!("Got wrong error"), + } + } + + #[test] + fn should_not_trigger_overflow_f64() { + decode_polyline::( include_str!("../resources/route-geometry-sweden-west-coast.polyline6"), 6, ) @@ -320,7 +375,16 @@ mod tests { } #[test] - fn limits() { + fn should_not_trigger_overflow_f32() { + decode_polyline::( + include_str!("../resources/route-geometry-sweden-west-coast.polyline6"), + 6, + ) + .unwrap(); + } + + #[test] + fn limits_f64() { let res: LineString = vec![[-180.0, -90.0], [180.0, 90.0], [0.0, 0.0]].into(); let polyline = "~fdtjD~niivI_oiivI__tsmT~fdtjD~niivI"; assert_eq!( @@ -330,6 +394,17 @@ mod tests { assert_eq!(decode_polyline(polyline, 6).unwrap(), res); } + #[test] + fn limits_f32() { + let res: LineString = vec![[-180.0, -90.0], [180.0, 90.0], [0.0, 0.0]].into(); + let polyline = "~fdtjD~niivI_oiivI__tsmT~fdtjD~niivI"; + assert_eq!( + encode_coordinates(res.coords().copied(), 6).unwrap(), + polyline + ); + assert_eq!(decode_polyline(polyline, 6).unwrap(), res); + } + #[test] fn truncated() { let input = LineString::from(vec![[2.0, 1.0], [4.0, 3.0]]); @@ -341,7 +416,27 @@ mod tests { assert_eq!(decode_polyline(polyline, 5).unwrap(), input); let truncated_polyline = "_ibE_seK_seK"; - let err = decode_polyline(truncated_polyline, 5).unwrap_err(); + let err = decode_polyline::(truncated_polyline, 5).unwrap_err(); + match err { + crate::errors::PolylineError::NoLongError { idx } => { + assert_eq!(idx, 8); + } + _ => panic!("Got wrong error"), + } + } + + #[test] + fn truncated_f32() { + let input = LineString::from(vec![[2.0f32, 1.0f32], [4.0f32, 3.0f32]]); + let polyline = "_ibE_seK_seK_seK"; + assert_eq!( + encode_coordinates(input.coords().copied(), 5).unwrap(), + polyline + ); + assert_eq!(decode_polyline(polyline, 5).unwrap(), input); + + let truncated_polyline = "_ibE_seK_seK"; + let err = decode_polyline::(truncated_polyline, 5).unwrap_err(); match err { crate::errors::PolylineError::NoLongError { idx } => { assert_eq!(idx, 8);