diff --git a/README.md b/README.md index 448e9be..06b60b3 100644 --- a/README.md +++ b/README.md @@ -36,15 +36,15 @@ use approx::assert_relative_eq; let longitude = -3.7038; let latitude = 40.4168; let resolution = 10_u8; -let qb = Cell::from_point(latitude, longitude, resolution); -assert_eq!(qb, Cell::new(5234261499580514303_u64)); +let qb = Cell::from_point(latitude, longitude, resolution).expect("cell index"); +assert_eq!(qb, Cell::try_from(5234261499580514303_u64).expect("cell index")); // Get a point from a Quadbin cell let coords = Cell::new(5209574053332910079_u64).to_point(); assert_eq!(coords, [-11.178401873711776, 33.75]); // Quadbin resolution at equator in mยฒ -let area = Cell::from_point(0.0, 0.0, 26).area_m2(); +let area = Cell::from_point(0.0, 0.0, 26).expect("cell index").area_m2(); assert_relative_eq!(area, 0.36, epsilon = 1e-2) ``` diff --git a/benches/decode_point.rs b/benches/decode_point.rs new file mode 100644 index 0000000..093c934 --- /dev/null +++ b/benches/decode_point.rs @@ -0,0 +1,21 @@ +use criterion::{Criterion, black_box}; +use qbin::Cell; + +const QBIN: u64 = 5246083350086549503; +const GEOHASH: &str = "rbsm1hsuvshv"; + +pub fn bench(c: &mut Criterion) { + let mut group = c.benchmark_group("decodePoint"); + + group.bench_function("geohash", |b| { + let (index, _, _) = geohash::decode(GEOHASH).unwrap(); + b.iter(|| black_box(&index)) + }); + + group.bench_function("qbin", |b| { + let index = Cell::new(QBIN).to_point(); + b.iter(|| black_box(&index)) + }); + + group.finish(); +} diff --git a/benches/encode_point.rs b/benches/encode_point.rs index 75f04b1..d4ac5fa 100644 --- a/benches/encode_point.rs +++ b/benches/encode_point.rs @@ -14,7 +14,7 @@ pub fn bench(c: &mut Criterion) { b.iter(|| black_box(&index)) }); group.bench_function("qbin", |b| { - let index = Cell::from_point(LAT, LNG, 12); + let index = Cell::from_point(LAT, LNG, 12).expect("cell index"); b.iter(|| black_box(&index)) }); diff --git a/benches/get_cell_area.rs b/benches/get_cell_area.rs index 4937f8f..41e1ae9 100644 --- a/benches/get_cell_area.rs +++ b/benches/get_cell_area.rs @@ -54,7 +54,7 @@ pub fn bench(c: &mut Criterion) { // Benchmark each resolution for quadbin for (i, &qb_index) in QUADBINS.iter().enumerate() { group.bench_with_input(BenchmarkId::new("qbin", i), &qb_index, |b, &index| { - let cell = Cell::new(index); + let cell = Cell::try_from(index).expect("cell index"); b.iter(|| black_box(cell).area_m2()) }); } diff --git a/benches/main.rs b/benches/main.rs index 896e2e7..ca1ef45 100644 --- a/benches/main.rs +++ b/benches/main.rs @@ -1,14 +1,16 @@ use criterion::{criterion_group, criterion_main}; +mod decode_point; mod encode_point; -mod get_cell_area; +// mod get_cell_area; mod get_resolution; criterion_group!( benches, get_resolution::bench, - get_cell_area::bench, - encode_point::bench + // get_cell_area::bench, + encode_point::bench, + decode_point::bench ); criterion_main!(benches); diff --git a/cliff.toml b/cliff.toml index 74e75df..3e06ed3 100644 --- a/cliff.toml +++ b/cliff.toml @@ -62,16 +62,16 @@ commit_preprocessors = [ commit_parsers = [ { message = "^feat", group = "๐Ÿš€ Features" }, { message = "^fix", group = "๐Ÿ› Bug Fixes" }, - { message = "^doc", group = "๐Ÿ“š Documentation", skip = true}, + { message = "^doc", group = "๐Ÿ“š Documentation" }, { message = "^perf", group = "โšก Performance" }, - { message = "^refactor", group = "๐Ÿšœ Refactor", skip = true}, + { message = "^refactor", group = "๐Ÿšœ Refactor" }, { message = "^style", group = "๐ŸŽจ Styling" }, - { message = "^test", group = "๐Ÿงช Testing", skip = true}, - { message = "^chore\\(release\\): prepare for", skip = true }, - { message = "^chore\\(deps.*\\)", skip = true }, - { message = "^chore\\(pr\\)", skip = true }, - { message = "^chore\\(pull\\)", skip = true }, - { message = "^chore|^ci", group = "โš™๏ธ Miscellaneous Tasks" , skip = true}, + { message = "^test", group = "๐Ÿงช Testing" }, + { message = "^chore\\(release\\): prepare for" }, + { message = "^chore\\(deps.*\\)" }, + { message = "^chore\\(pr\\)" }, + { message = "^chore\\(pull\\)" }, + { message = "^chore|^ci", group = "โš™๏ธ Miscellaneous Tasks" }, { body = ".*security", group = "๐Ÿ›ก๏ธ Security" }, { message = "^revert", group = "โ—€๏ธ Revert" }, { message = ".*", group = "๐Ÿ’ผ Other" }, diff --git a/src/cells.rs b/src/cells.rs index f9c8e8c..2a7cae7 100644 --- a/src/cells.rs +++ b/src/cells.rs @@ -1,5 +1,7 @@ use crate::Direction; use crate::constants::*; +use crate::errors; +use crate::errors::*; use crate::tiles::*; use crate::utils::*; use core::{fmt, num::NonZeroU64}; @@ -26,6 +28,21 @@ use core::{fmt, num::NonZeroU64}; #[derive(Debug, PartialEq, Eq, Copy, Clone)] pub struct Cell(NonZeroU64); +impl TryFrom for Cell { + type Error = errors::QuadbinError; + + fn try_from(value: u64) -> Result { + if !is_valid_cell(value) { + return Err(QuadbinError::InvalidCell(InvalidCell::new( + Some(value), + "Provided Quadbin Cell index is invalid", + ))); + } + + Ok(Self(NonZeroU64::new(value).expect("non-zero cell index"))) + } +} + impl Cell { /// Returns the inner u64 value of the cell. pub fn get(&self) -> u64 { @@ -34,34 +51,27 @@ impl Cell { /// Create new Quadbin cell from index. /// - /// # Example - /// ``` - /// let qb_cell = qbin::Cell::new(5234261499580514303); - /// ``` - pub fn new(value: u64) -> Cell { - assert!( - is_valid_cell(value), - "Provided Quadbin Cell index is invalid" - ); - Cell(NonZeroU64::new(value).expect("non-zero cell index")) - } - - /// Quadbin cell index validation. + /// A shortcut for [Cell::try_from()]. /// /// # Example /// ``` - /// let qb_cell = qbin::Cell::new(5234261499580514303); - /// assert_eq!(qb_cell.is_valid(), true) + /// use qbin::Cell; + /// + /// let cell_new = Cell::new(5234261499580514303); + /// let cell_try = Cell::try_from(5234261499580514303).expect("cell index"); + /// assert_eq!(cell_new, cell_try); /// ``` - pub fn is_valid(&self) -> bool { - is_valid_cell(self.get()) + pub fn new(value: u64) -> Self { + Cell::try_from(value).expect("cell index") } /// Returns the resolution of the cell index. /// /// # Example /// ``` - /// let qb_cell = qbin::Cell::new(5234261499580514303); + /// use qbin::Cell; + /// + /// let qb_cell = Cell::try_from(5234261499580514303).expect("cell index"); /// let res = qb_cell.resolution(); /// assert_eq!(res, 10) /// ``` @@ -73,11 +83,13 @@ impl Cell { /// /// # Example /// ``` - /// let qb_cell = qbin::Cell::new(5209574053332910079); - /// let parent = qb_cell.parent(2_u8); - /// assert_eq!(parent, qbin::Cell::new(5200813144682790911)) + /// use qbin::Cell; + /// + /// let qb_cell = Cell::try_from(5209574053332910079).expect("cell index"); + /// let parent = qb_cell.parent(2_u8).expect("cell index"); + /// assert_eq!(parent, Cell::try_from(5200813144682790911).expect("cell index")) /// ``` - pub fn parent(&self, parent_res: u8) -> Cell { + pub fn parent(&self, parent_res: u8) -> Result { cell_to_parent(self, parent_res) } @@ -92,24 +104,26 @@ impl Cell { /// /// See [Direction] for allowed arguments. /// + /// Return `None` if there is no neighbor in this [Direction]. + /// /// # Example /// ``` /// use qbin::{Cell, Direction}; /// - /// let sibling = Cell::new(5209574053332910079).neighbor(Direction::Right); + /// let cell = Cell::try_from(5209574053332910079).expect("cell index"); + /// let sibling = cell.neighbor(Direction::Right); /// assert_eq!(sibling, Some(Cell::new(5209626829891043327))); /// ``` pub fn neighbor(&self, direction: Direction) -> Option { - let tile = self.to_tile().neighbor(direction); - tile.map(Tile::to_cell) + let tile = self.to_tile().neighbor(direction)?; + tile.to_cell().ok() } /// Find the Cell's sibling in a specific [Direction]. /// /// See [Cell::neighbor]. pub fn sibling(&self, direction: Direction) -> Option { - let tile = self.to_tile().neighbor(direction); - tile.map(Tile::to_cell) + self.neighbor(direction) } /// List all Cell's neighbors. @@ -133,8 +147,10 @@ impl Cell { /// # Example /// ``` /// use approx::assert_relative_eq; + /// use qbin::Cell; /// - /// let area = qbin::Cell::new(5234261499580514303_u64).area_m2(); + /// let my_cell = Cell::try_from(5234261499580514303_u64).expect("cell index"); + /// let area = my_cell.area_m2(); /// assert_relative_eq!(area, 888546364.7859862, epsilon = 1e-6) /// /// ``` @@ -149,8 +165,10 @@ impl Cell { /// # Example /// ``` /// use approx::assert_relative_eq; + /// use qbin::Cell; /// - /// let area = qbin::Cell::new(5234261499580514303_u64).area_km2(); + /// let my_cell = Cell::try_from(5234261499580514303_u64).expect("cell index"); + /// let area = my_cell.area_km2(); /// assert_relative_eq!(area, 888.5463647859862, epsilon = 1e-6) /// /// ``` @@ -166,7 +184,8 @@ impl Cell { /// ``` /// use qbin::Cell; /// - /// let coords = Cell::new(5209574053332910079_u64).to_point(); + /// let cell = Cell::try_from(5209574053332910079).expect("cell index"); + /// let coords = cell.to_point(); /// assert_eq!(coords, [-11.178401873711776, 33.75]); /// ``` /// @@ -181,16 +200,19 @@ impl Cell { /// /// # Example /// ``` - /// let bbox = qbin::Cell::new(5209574053332910079).to_bbox(); + /// use qbin::Cell; + /// + /// let cell = Cell::try_from(5209574053332910079).expect("cell index"); + /// let bbox = cell.to_bbox(); /// assert_eq!( bbox, [22.5, -21.943045533438166, 45.0, 0.0]) /// ``` pub fn to_bbox(&self) -> [f64; 4] { - let tile = self.to_tile(); + let tile = &self.to_tile(); - let xmin = tile.to_longitude(0.0); - let xmax = tile.to_longitude(1.0); - let ymin = tile.to_latitude(1.0); - let ymax = tile.to_latitude(0.0); + let xmin = tile.to_longitude(0.0).expect("offset"); + let xmax = tile.to_longitude(1.0).expect("offset"); + let ymin = tile.to_latitude(1.0).expect("offset"); + let ymax = tile.to_latitude(0.0).expect("offset"); [xmin, ymin, xmax, ymax] } @@ -200,10 +222,12 @@ impl Cell { /// # Example /// /// ``` - /// let cell = qbin::Cell::from_point(-41.28303675124842, 174.77727344223067, 26); + /// use qbin::Cell; + /// + /// let cell = Cell::from_point(-41.28303675124842, 174.77727344223067, 26).expect("cell index"); /// assert_eq!(cell.get(), 5309133744805926483_u64) /// ``` - pub fn from_point(lat: f64, lng: f64, res: u8) -> Cell { + pub fn from_point(lat: f64, lng: f64, res: u8) -> Result { point_to_cell(lat, lng, res) } @@ -223,9 +247,8 @@ impl fmt::Display for Cell { // Detect direction from neighbor https://github.com/HydroniumLabs/h3o/blob/ad2bebf52eab218d66b0bf213b14a2802bf616f7/src/base_cell.rs#L135C1-L150C6 // Internal functions ------------------------------------------------ - /// Quadbin cell validation -pub(crate) fn is_valid_cell(cell64: u64) -> bool { +fn is_valid_cell(cell64: u64) -> bool { let header = HEADER; let mode = (cell64 >> 59) & 7; let resolution = (cell64 >> 52) & 0x1F; @@ -241,7 +264,7 @@ pub(crate) fn is_valid_cell(cell64: u64) -> bool { } /// Convert a tile into a Quadbin cell. -pub(crate) fn tile_to_cell(tile: Tile) -> Cell { +pub(crate) fn tile_to_cell(tile: Tile) -> Result { let mut x = tile.x as u64; let mut y = tile.y as u64; let z = tile.z as u64; @@ -265,13 +288,11 @@ pub(crate) fn tile_to_cell(tile: Tile) -> Cell { y = (y | (y << S[0])) & B[0]; let cell = HEADER | (1 << 59) | (z << 52) | ((x | (y << 1)) >> 12) | (FOOTER >> (z * 2)); - Cell::new(cell) + Cell::try_from(cell) } /// Convert Quadbin cell into a tile -pub(crate) fn cell_to_tile(cell: &Cell) -> Tile { - assert!(cell.is_valid(), "Quadbin cell index is not valid"); - +fn cell_to_tile(cell: &Cell) -> Tile { let cell64 = cell.get(); let z = (cell64 >> 52) & 31; let q = (cell64 & FOOTER) << 12; @@ -303,7 +324,7 @@ pub(crate) fn cell_to_tile(cell: &Cell) -> Tile { } /// Convert a geographic point into a cell. -pub(crate) fn point_to_cell(lat: f64, lng: f64, res: u8) -> Cell { +fn point_to_cell(lat: f64, lng: f64, res: u8) -> Result { let lng = clip_longitude(lng); let lat = clip_latitude(lat); @@ -313,12 +334,10 @@ pub(crate) fn point_to_cell(lat: f64, lng: f64, res: u8) -> Cell { } /// Convert cell into point -pub(crate) fn cell_to_point(cell: &Cell) -> [f64; 2] { - assert!(cell.is_valid(), "Quadbin cell index is not valid"); - +fn cell_to_point(cell: &Cell) -> [f64; 2] { let tile = cell.to_tile(); - let lat = tile.to_latitude(0.5); - let lon = tile.to_longitude(0.5); + let lat = tile.to_latitude(0.5).expect("offset"); + let lon = tile.to_longitude(0.5).expect("offset"); // Return array, not tuple, as it more memory efficient // See https://doc.rust-lang.org/stable/book/ch03-02-data-types.html#the-array-type @@ -326,17 +345,19 @@ pub(crate) fn cell_to_point(cell: &Cell) -> [f64; 2] { } /// Compute the parent cell for a specific resolution. -pub(crate) fn cell_to_parent(cell: &Cell, parent_res: u8) -> Cell { +fn cell_to_parent(cell: &Cell, parent_res: u8) -> Result { // Check resolution let resolution = cell.resolution(); - assert!( - parent_res < resolution, - "parent resolution should be greater than current resolution" - ); + if parent_res >= resolution { + return Err(QuadbinError::InvalidResolution(InvalidResolution::new( + parent_res, + "Parent resolution should be lower than the current resolution", + ))); + } let result = (cell.get() & !(0x1F << 52)) | ((parent_res as u64) << 52) | (FOOTER >> ((parent_res as u64) << 1)); - Cell::new(result) + Cell::try_from(result) } diff --git a/src/errors.rs b/src/errors.rs index 5e72eca..29fd8c6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -45,3 +45,26 @@ macro_rules! invalid_value_error { } invalid_value_error!("direction", InvalidDirection, u8); +invalid_value_error!("cell index", InvalidCell, Option); +invalid_value_error!("resolution", InvalidResolution, u8); +invalid_value_error!("offset", InvalidOffset, f64); + +// One enum to rule them all +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum QuadbinError { + InvalidResolution(InvalidResolution), + InvalidOffset(InvalidOffset), + InvalidCell(InvalidCell), +} + +impl std::fmt::Display for QuadbinError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + QuadbinError::InvalidResolution(e) => write!(f, "{}", e), + QuadbinError::InvalidOffset(e) => write!(f, "{}", e), + QuadbinError::InvalidCell(e) => write!(f, "{}", e), + } + } +} + +impl std::error::Error for QuadbinError {} diff --git a/src/lib.rs b/src/lib.rs index 0af4a97..a367a87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![doc = include_str!("../README.md")] // Quadbin cell itself -pub mod cells; +mod cells; pub use crate::cells::Cell; // Direction struct diff --git a/src/test/cells.rs b/src/test/cells.rs index 9b09b14..fa6f54d 100644 --- a/src/test/cells.rs +++ b/src/test/cells.rs @@ -9,29 +9,6 @@ const DOWN: Direction = Direction::Down; const RIGHT: Direction = Direction::Right; const LEFT: Direction = Direction::Left; -// Test validiy of Quadbin cell indexes -#[test] -fn test_is_cell_valid() { - let cases = [ - (5209574053332910079_u64, true), - (5192650370358181887_u64, true), - (5202361257054699519_u64, true), - (5291729562728627583_u64, true), - ]; - - for (cell, expected) in cases.iter() { - assert_eq!(Cell::new(*cell).is_valid(), *expected); - } -} - -// Expect panic due to invalid cell index -#[test] -#[should_panic(expected = "Provided Quadbin Cell index is invalid")] -fn test_new_invalid_cell() { - let _ = Cell::new(5209574053332910078_u64); - let _ = Cell::new(6362495557939757055_u64); -} - // Validation test from original CARTO's `quadbin-js` // https://github.com/CartoDB/quadbin-js/blob/40cce2fc6b9dc72bf19c69ffb6705f8b73d24b2c/test/index.spec.ts#L30-L34 #[test] @@ -46,12 +23,18 @@ fn test_tile_and_cell_conversion() { // Tile to cell conversion for (x, y, z, cell) in cases.iter() { - assert_eq!(Tile::new(*x, *y, *z).to_cell(), Cell::new(*cell)); + assert_eq!( + Tile::new(*x, *y, *z).to_cell().expect("cell index"), + Cell::try_from(*cell).expect("cell index") + ); } // Cell to tile conversion for (x, y, z, cell) in cases.iter() { - assert_eq!(Cell::new(*cell).to_tile(), Tile::new(*x, *y, *z)); + assert_eq!( + Cell::try_from(*cell).expect("cell index").to_tile(), + Tile::new(*x, *y, *z) + ); } } @@ -76,7 +59,10 @@ fn test_point_to_cell() { ]; for (x, y, res, cell) in cases.iter() { - assert_eq!(Cell::from_point(*y, *x, *res), Cell::new(*cell)); + assert_eq!( + Cell::from_point(*y, *x, *res).expect("cell index"), + Cell::try_from(*cell).expect("cell index") + ); } } @@ -85,7 +71,7 @@ fn test_point_to_cell() { fn test_cell_to_bbox() { // Conversion works assert_eq!( - Cell::new(5209574053332910079).to_bbox(), + Cell::try_from(5209574053332910079).unwrap().to_bbox(), [22.5, -21.943045533438166, 45.0, 0.0] ); @@ -98,7 +84,7 @@ fn test_cell_to_bbox() { ]; for i in cases.iter() { - let bbox = Cell::new(*i).to_bbox(); + let bbox = Cell::try_from(*i).unwrap().to_bbox(); assert!(bbox[0] < bbox[2]); assert!(bbox[1] < bbox[3]); } @@ -108,11 +94,15 @@ fn test_cell_to_bbox() { #[test] fn test_cell_to_point() { assert_eq!( - Cell::new(5209574053332910079_u64).to_point(), + Cell::try_from(5209574053332910079_u64) + .expect("cell index") + .to_point(), [-11.178401873711776, 33.75] ); - let coords = Cell::new(5309133744805926483_u64).to_point(); + let coords = Cell::try_from(5309133744805926483_u64) + .expect("cell index") + .to_point(); assert_relative_eq!(coords[0], -41.28303708488909, epsilon = 1e-6); assert_relative_eq!(coords[1], 174.77727502584457, epsilon = 1e-6) } @@ -120,7 +110,7 @@ fn test_cell_to_point() { // Get cell resolution #[test] fn test_get_cell_resolution() { - let qb_cell = Cell::new(5209574053332910079_u64); + let qb_cell = Cell::try_from(5209574053332910079_u64).expect("cell index"); assert_eq!(qb_cell.resolution(), 4_u8) } @@ -133,20 +123,16 @@ fn test_cell_to_parent() { ]; for (cell, res, parent) in cases.iter() { - assert_eq!(Cell::new(*cell).parent(*res), Cell::new(*parent)); + assert_eq!(Cell::new(*cell).parent(*res).unwrap(), Cell::new(*parent)); } } -#[test] -#[should_panic(expected = "parent resolution should be greater than current resolution")] -fn test_cell_to_parent_invalid_resolution() { - let cell = Cell::new(5209574053332910079); - let _ = cell.parent(4); -} // Estimate cell area #[test] fn test_cell_area() { - let area = Cell::new(5209574053332910079_u64).area_m2(); + let area = Cell::try_from(5209574053332910079_u64) + .expect("cell index") + .area_m2(); assert_relative_eq!(area, 6023040823252.664, epsilon = 1e-2); } @@ -155,30 +141,60 @@ fn test_cell_area() { // https://github.com/CartoDB/quadbin-py/blob/39a0adbb238ff214fbbca7b73200cfebf2aef38c/tests/unit/test_main.py#L203 #[test] fn test_cell_neighbor() { - assert_eq!(Cell::new(5192650370358181887).neighbor(UP), None); - assert_eq!(Cell::new(5193776270265024511).neighbor(UP), None); - assert_eq!(Cell::new(5194902170171867135).neighbor(UP), None); - assert_eq!(Cell::new(5194902170171867135).neighbor(RIGHT), None); + assert_eq!( + Cell::try_from(5192650370358181887) + .expect("cell index") + .neighbor(UP), + None + ); + assert_eq!( + Cell::try_from(5193776270265024511) + .expect("cell index") + .neighbor(UP), + None + ); + assert_eq!( + Cell::try_from(5194902170171867135) + .expect("cell index") + .neighbor(UP), + None + ); + assert_eq!( + Cell::try_from(5194902170171867135) + .expect("cell index") + .neighbor(RIGHT), + None + ); // Resolution 1 assert_eq!( - Cell::new(5193776270265024511).neighbor(DOWN), - Some(Cell::new(5196028070078709759)) + Cell::try_from(5193776270265024511) + .expect("cell index") + .neighbor(DOWN), + Some(Cell::try_from(5196028070078709759).expect("cell index")) ); assert_eq!( - Cell::new(5193776270265024511).neighbor(RIGHT), + Cell::try_from(5193776270265024511) + .expect("cell index") + .neighbor(RIGHT), Some(Cell::new(5194902170171867135)) ); assert_eq!( - Cell::new(5194902170171867135).neighbor(DOWN), + Cell::try_from(5194902170171867135) + .expect("cell index") + .neighbor(DOWN), Some(Cell::new(5197153969985552383)) ); assert_eq!( - Cell::new(5194902170171867135).neighbor(LEFT), + Cell::try_from(5194902170171867135) + .expect("cell index") + .neighbor(LEFT), Some(Cell::new(5193776270265024511)) ); assert_eq!( - Cell::new(5209574053332910079).neighbor(UP), + Cell::try_from(5209574053332910079) + .expect("cell index") + .neighbor(UP), Some(Cell::new(5208061125333090303)) ); @@ -201,10 +217,10 @@ fn test_cell_neighbor() { #[test] fn test_cell_neighbors() { let center_cells = [ - Cell::new(5209574053332910079), - Cell::new(5194902170171867135), - Cell::new(5192650370358181887), - Cell::new(5201094619659501567), + Cell::try_from(5209574053332910079).expect("cell index"), + Cell::try_from(5194902170171867135).expect("cell index"), + Cell::try_from(5192650370358181887).expect("cell index"), + Cell::try_from(5201094619659501567).expect("cell index"), ]; for i in center_cells.iter() { @@ -220,6 +236,8 @@ fn test_cell_neighbors() { } // Test that None is returned alongside with Some(Cell) - let nn = Cell::new(5201094619659501567).neighbors(); + let nn = Cell::try_from(5201094619659501567) + .expect("cell index") + .neighbors(); assert_eq!(nn[1], None) } diff --git a/src/test/errors.rs b/src/test/errors.rs new file mode 100644 index 0000000..625efb4 --- /dev/null +++ b/src/test/errors.rs @@ -0,0 +1,58 @@ +use crate::Cell; +use crate::errors::*; + +#[test] +fn test_invalid_cellindex() { + assert!( + !InvalidCell::new(Some(5209574053332910078_u64), "error") + .to_string() + .is_empty() + ); + assert_eq!( + Cell::try_from(5209574053332910078_u64).err(), + Some(QuadbinError::InvalidCell(InvalidCell::new( + Some(5209574053332910078_u64), + "Provided Quadbin Cell index is invalid" + ))) + ); + assert_eq!( + Cell::try_from(5209574053332910079_u64) + .expect("cell index") + .get(), + 5209574053332910079_u64 + ); +} + +#[test] +fn test_cell_to_parent_invalid_resolution() { + let cell = Cell::try_from(5209574053332910079).expect("cell index"); + let result = cell.parent(4); + + assert!(result.is_err()); + + assert_eq!( + result.err(), + Some(QuadbinError::InvalidResolution(InvalidResolution::new( + 4, + "Parent resolution should be lower than the current resolution" + ))) + ); +} + +#[test] +fn test_invalid_cell_index() { + let val: [u64; 2] = [5209574053332910078, 6362495557939757055]; + + for i in val.iter() { + let cell = Cell::try_from(*i); + assert!(cell.is_err()); + + assert_eq!( + cell.err(), + Some(QuadbinError::InvalidCell(InvalidCell::new( + Some(*i), + "Provided Quadbin Cell index is invalid" + ))) + ); + } +} diff --git a/src/test/mod.rs b/src/test/mod.rs index 191cc0d..c299d71 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,4 +1,5 @@ mod cells; mod directions; +mod errors; mod hashing; mod tiles; diff --git a/src/test/tiles.rs b/src/test/tiles.rs index ab8360a..adbb49e 100644 --- a/src/test/tiles.rs +++ b/src/test/tiles.rs @@ -10,7 +10,8 @@ const ACC: f64 = 1e-10; // See https://github.com/CartoDB/quadbin-py/blob/master/tests/unit/test_utils.py #[test] fn test_point_to_tile_fraction() { - let tile = point_to_tile_fraction(41.26000108568697_f64, -95.93965530395508_f64, 9_u8); + let tile = point_to_tile_fraction(41.26000108568697_f64, -95.93965530395508_f64, 9_u8) + .expect("resolution"); assert_relative_eq!(tile.0, 119.552490234375_f64, epsilon = ACC); assert_relative_eq!(tile.1, 191.47119140625_f64, epsilon = ACC); assert_eq!(tile.2, 9_u8); @@ -69,16 +70,16 @@ fn test_tile_conversion() { assert_eq!(tile.z, 10_u8); // Convert back to coordinates - let new_lon = tile.to_longitude(0.0); - let new_lat = tile.to_latitude(0.0); + let new_lon = tile.to_longitude(0.0).expect("offset"); + let new_lat = tile.to_latitude(0.0).expect("offset"); // Check conversion with approximate equality assert_relative_eq!(new_lat, 45.08903556483104_f64, epsilon = ACC); assert_relative_eq!(new_lon, lon, epsilon = ACC); // Check offset with approximate equality - let new_lon_offset = tile.to_longitude(0.5); - let new_lat_offset = tile.to_latitude(0.5); + let new_lon_offset = tile.to_longitude(0.5).expect("offset"); + let new_lat_offset = tile.to_latitude(0.5).expect("offset"); assert_relative_eq!(new_lat_offset, 44.96479793033102_f64, epsilon = ACC); assert_relative_eq!(new_lon_offset, -44.82421875_f64, epsilon = ACC); } diff --git a/src/tiles.rs b/src/tiles.rs index 2974109..ca43845 100644 --- a/src/tiles.rs +++ b/src/tiles.rs @@ -1,5 +1,6 @@ use crate::Direction; use crate::cells::*; +use crate::errors::QuadbinError; use crate::utils::*; /// A single tile coordinates @@ -19,7 +20,7 @@ impl Tile { } /// Convert to Quadbin cell. - pub fn to_cell(self) -> Cell { + pub fn to_cell(self) -> Result { tile_to_cell(self) } @@ -37,16 +38,16 @@ impl Tile { /// /// See also [Tile::to_longitude]. /// - pub fn to_latitude(self, offset: f64) -> f64 { - tile_to_latitude(&self, offset) + pub fn to_latitude(self, offset: f64) -> Option { + tile_to_latitude(&self, offset).ok() } /// Return tile's longitude. /// /// See also [Tile::to_latitude]. /// - pub fn to_longitude(self, offset: f64) -> f64 { - tile_to_longitude(&self, offset) + pub fn to_longitude(self, offset: f64) -> Option { + tile_to_longitude(&self, offset).ok() } /// Get tile's siblings. diff --git a/src/utils.rs b/src/utils.rs index 31d4cc1..3eab058 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,6 @@ use crate::constants::*; use crate::directions::Direction; +use crate::errors::{InvalidOffset, InvalidResolution, QuadbinError}; use crate::tiles::Tile; use std::f64::consts::PI; @@ -20,12 +21,18 @@ pub(crate) fn clip_latitude(lat: f64) -> f64 { /// Compute the tile in fractions for a longitude and latitude in a /// specific resolution. -pub(crate) fn point_to_tile_fraction(lat: f64, lng: f64, res: u8) -> (f64, f64, u8) { +pub(crate) fn point_to_tile_fraction( + lat: f64, + lng: f64, + res: u8, +) -> Result<(f64, f64, u8), QuadbinError> { // Check resolution to avoid overflow - assert!( - (res <= MAX_RESOLUTION), - "Resolution should be between 0 and 26" - ); + if res > MAX_RESOLUTION { + return Err(QuadbinError::InvalidResolution(InvalidResolution::new( + res, + "Resolution should be between 0 and 26", + ))); + } // Compute tile coordinates let z2: f64 = (1 << res) as f64; @@ -38,24 +45,26 @@ pub(crate) fn point_to_tile_fraction(lat: f64, lng: f64, res: u8) -> (f64, f64, let x = if x < 0.0 { x + z2 } else { x }; // Return the tile coordinates - (x, y, res) + Ok((x, y, res)) } /// Compute the tile for a longitude and latitude in a specific resolution. pub(crate) fn point_to_tile(lat: f64, lng: f64, res: u8) -> Tile { - let (x, y, z) = point_to_tile_fraction(lat, lng, res); + let (x, y, z) = point_to_tile_fraction(lat, lng, res).expect("resolution"); let x: u32 = x.floor() as u32; let y: u32 = y.floor() as u32; Tile::new(x, y, z) } /// Compute the latitude for a tile with an offset. -pub(crate) fn tile_to_latitude(tile: &Tile, offset: f64) -> f64 { +pub(crate) fn tile_to_latitude(tile: &Tile, offset: f64) -> Result { // Check if offset is between 0 and 1 - assert!( - (0.0..=1.0).contains(&offset), - "Offset should be between 0 and 1" - ); + if !(0.0..=1.0).contains(&offset) { + return Err(QuadbinError::InvalidOffset(InvalidOffset::new( + offset, + "Offset should be between 0.0 and 1.0", + ))); + } // Get Tile coords let y = tile.y as f64; @@ -63,23 +72,26 @@ pub(crate) fn tile_to_latitude(tile: &Tile, offset: f64) -> f64 { // Compute latitude let expy = f64::exp(-(2.0 * (y + offset) / z2 - 1.0) * PI); - 360.0 * (f64::atan(expy) / PI - 0.25) + let lat = 360.0 * (f64::atan(expy) / PI - 0.25); + Ok(lat) } /// Compute the longitude for a tile with an offset. -pub(crate) fn tile_to_longitude(tile: &Tile, offset: f64) -> f64 { +pub(crate) fn tile_to_longitude(tile: &Tile, offset: f64) -> Result { // Check if offset is between 0 and 1 - assert!( - (0.0..=1.0).contains(&offset), - "Offset should be between 0 and 1" - ); + if !(0.0..=1.0).contains(&offset) { + return Err(QuadbinError::InvalidOffset(InvalidOffset::new( + offset, + "Offset should be between 0.0 and 1.0", + ))); + } // Get Tile coords let x = tile.x as f64; let z2 = (1 << tile.z) as f64; // Compute longitude - 180.0 * (2.0 * (x + offset) / z2 - 1.0) + Ok(180.0 * (2.0 * (x + offset) / z2 - 1.0)) } /// Inverse of the scale factor at the tile center.