From d8c5009c0b1c48868632ba080e30ec80c0660feb Mon Sep 17 00:00:00 2001 From: Anatolii Tsyplenkov Date: Sun, 27 Apr 2025 11:32:42 +1200 Subject: [PATCH 01/10] docs: README update --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 25 ++++++++++++++++++++----- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 009cbf4..a1edff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -386,7 +386,7 @@ dependencies = [ [[package]] name = "qbin" -version = "0.1.0" +version = "0.2.0" dependencies = [ "approx", "criterion", diff --git a/Cargo.toml b/Cargo.toml index c7068e9..37c969a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qbin" -version = "0.1.0" +version = "0.2.0" authors = ["Anatoly Tsyplenkov "] edition = "2024" license = "MIT" diff --git a/README.md b/README.md index 02d297e..29614ac 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,25 @@ # Quadbin -[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/atsyplenkov/qbin/blob/main/LICENSE) -[![crates.io](https://img.shields.io/crates/v/qbin.svg?logo=rust)](https://crates.io/crates/qbin) -[![Docs.rs](https://img.shields.io/docsrs/qbin?label=docs.rs&logo=rust)](https://docs.rs/qbin) -[![Build & Test](https://github.com/atsyplenkov/qbin/actions/workflows/rust.yml/badge.svg)](https://github.com/atsyplenkov/qbin/actions/workflows/rust.yml) -[![codecov](https://codecov.io/gh/atsyplenkov/qbin/graph/badge.svg?token=4SZ4RI3ILS)](https://codecov.io/gh/atsyplenkov/qbin) +

+ + + + + + +
+ + + + +
+

+ +

+ Documentation | + Website +

+ A Rust implementation of Quadbin, a hierarchical geospatial index tiling approach developed by [CARTO](https://github.com/CartoDB). Like the [Microsoft's Bing Maps Tile System](https://docs.microsoft.com/en-us/bingmaps/articles/bing-maps-tile-system) (aka Quadkey), Quadbin uniformly subdivides a map in Mercator projection into four squares at different resolution levels, from 0 to 26 (less than 1 m² at the equator). However, unlike Quadkey, Quadbin stores the grid cell index in a 64-bit integer. From cbf73a9f4089f79b7473ea91d65c43582e0214df Mon Sep 17 00:00:00 2001 From: Anatolii Tsyplenkov Date: Sun, 27 Apr 2025 12:00:33 +1200 Subject: [PATCH 02/10] bench: benchmarking with geohash --- Cargo.lock | 23 +++++++++++++++++++++++ Cargo.toml | 1 + benches/encode_point.rs | 21 +++++++++++++++++++++ benches/main.rs | 8 +++++++- 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 benches/encode_point.rs diff --git a/Cargo.lock b/Cargo.lock index a1edff4..d7c19a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,6 +220,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28a80e3145d8ad11ba0995949bbcf48b9df2be62772b3d351ef017dff6ecb853" +[[package]] +name = "geo-types" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ddb1950450d67efee2bbc5e429c68d052a822de3aad010d28b351fbb705224" +dependencies = [ + "approx", + "num-traits", + "serde", +] + +[[package]] +name = "geohash" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fb94b1a65401d6cbf22958a9040aa364812c26674f841bee538b12c135db1e6" +dependencies = [ + "geo-types", + "libm", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -333,6 +354,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -390,6 +412,7 @@ version = "0.2.0" dependencies = [ "approx", "criterion", + "geohash", "h3o", ] diff --git a/Cargo.toml b/Cargo.toml index 37c969a..ba17723 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ categories = ["data-structures", "science::geo"] [dev-dependencies] approx = "0.5.1" criterion = "0.5.1" +geohash = "0.13.1" h3o = "0.8.0" [[bench]] diff --git a/benches/encode_point.rs b/benches/encode_point.rs new file mode 100644 index 0000000..3d6ae53 --- /dev/null +++ b/benches/encode_point.rs @@ -0,0 +1,21 @@ +use criterion::{Criterion, black_box}; +use qbin::Cell; + +const LAT: f64 = -41.28303675124842; +const LNG: f64 = 174.77727344223067; + +pub fn bench(c: &mut Criterion) { + let mut group = c.benchmark_group("encodePoint"); + + group.bench_function("geohash", |b| { + let coord = geohash::Coord { x: LNG, y: LAT }; + let index = geohash::encode(coord, 12).expect("Invalid coordinate"); + b.iter(|| black_box(&index)) + }); + group.bench_function("qbin", |b| { + let index = Cell::from_point(LAT, LNG, 12); + b.iter(|| black_box(&index)) + }); + + group.finish(); +} diff --git a/benches/main.rs b/benches/main.rs index 78f9096..896e2e7 100644 --- a/benches/main.rs +++ b/benches/main.rs @@ -1,8 +1,14 @@ use criterion::{criterion_group, criterion_main}; +mod encode_point; mod get_cell_area; mod get_resolution; -criterion_group!(benches, get_resolution::bench, get_cell_area::bench); +criterion_group!( + benches, + get_resolution::bench, + get_cell_area::bench, + encode_point::bench +); criterion_main!(benches); From 7bd5ccc55e254951daa8cf4890b1ef408878885a Mon Sep 17 00:00:00 2001 From: Anatolii Tsyplenkov Date: Sun, 27 Apr 2025 20:56:41 +1200 Subject: [PATCH 03/10] bench: add point encoding --- benches/encode_point.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/benches/encode_point.rs b/benches/encode_point.rs index 3d6ae53..75f04b1 100644 --- a/benches/encode_point.rs +++ b/benches/encode_point.rs @@ -1,4 +1,5 @@ use criterion::{Criterion, black_box}; +use h3o::{LatLng, Resolution}; use qbin::Cell; const LAT: f64 = -41.28303675124842; @@ -17,5 +18,11 @@ pub fn bench(c: &mut Criterion) { b.iter(|| black_box(&index)) }); + group.bench_function("h3o", |b| { + let latlng = LatLng::new(LAT, LNG).expect("Invalid coordinate"); + let index = latlng.to_cell(Resolution::Twelve); + b.iter(|| black_box(&index)) + }); + group.finish(); } From 0b5c3bd12fcd1a4183c5b5bb36c133cd2177ae0f Mon Sep 17 00:00:00 2001 From: Anatolii Tsyplenkov Date: Sun, 27 Apr 2025 20:58:45 +1200 Subject: [PATCH 04/10] ci: add clippy to CI --- .github/workflows/rust.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b66d667..220cbd9 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -24,4 +24,6 @@ jobs: - name: Build run: cargo build --verbose - name: Run tests - run: cargo test --verbose -- --test-threads=2 \ No newline at end of file + run: cargo test --verbose -- --test-threads=2 + - name: Run linter + run: cargo clippy --verbose \ No newline at end of file From c2e93fef711c05c6892a08a492014872da626f7a Mon Sep 17 00:00:00 2001 From: Anatolii Tsyplenkov Date: Sun, 27 Apr 2025 21:09:37 +1200 Subject: [PATCH 05/10] docs: README update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 29614ac..1e5ac43 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@
- + - +

From fbf43034dd569c25a156a889740ec8f2a9d67b39 Mon Sep 17 00:00:00 2001 From: Anatolii Tsyplenkov Date: Tue, 29 Apr 2025 11:28:45 +1200 Subject: [PATCH 06/10] refactor: renamed and relocated structs and supporting functions into separate files --- Cargo.toml | 1 + src/cells.rs | 226 +++++++++++++++++++++- src/{direction.rs => directions.rs} | 4 +- src/{error.rs => errors.rs} | 0 src/lib.rs | 19 +- src/test/cells.rs | 5 +- src/test/direction.rs | 4 +- src/test/hashing.rs | 2 +- src/test/utils.rs | 4 +- src/tiles.rs | 68 +++++++ src/types.rs | 289 ---------------------------- src/utils.rs | 4 +- 12 files changed, 316 insertions(+), 310 deletions(-) rename src/{direction.rs => directions.rs} (97%) rename src/{error.rs => errors.rs} (100%) create mode 100644 src/tiles.rs delete mode 100644 src/types.rs diff --git a/Cargo.toml b/Cargo.toml index ba17723..19fef98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ keywords = ["quadbin", "quadkey", "spatial", "spatial-index"] categories = ["data-structures", "science::geo"] [dependencies] +geohash = "0.13.1" [dev-dependencies] approx = "0.5.1" diff --git a/src/cells.rs b/src/cells.rs index 9735492..f9c8e8c 100644 --- a/src/cells.rs +++ b/src/cells.rs @@ -1,6 +1,228 @@ +use crate::Direction; use crate::constants::*; -use crate::types::*; +use crate::tiles::*; use crate::utils::*; +use core::{fmt, num::NonZeroU64}; + +/// Represents a cell in the Quadbin grid system at a +/// particular resolution. +/// +/// The index is encoded on 64-bit with the following bit layout: +/// +/// ```text +/// ┏━┳━━━┳━━━━┳━━━━━━━┳━━━━━━━━━━━┈┈┈┈┈┈┈┈━━━━━━━━┓ +/// ┃U┃ H ┃ M ┃ R ┃ XY in Morton order ┃ +/// ┗━┻━━━┻━━━━┻━━━━━━━┻━━━━━━━━━━━┈┈┈┈┈┈┈┈━━━━━━━━┛ +/// 63 62 59 56 52 0 +/// ``` +/// +/// Where: +/// - `U`: Unused reserved bit (bit 63), always set to `0`; +/// - `H`: Header bit (bit 62), always set to `1`; +/// - `M`: Index mode, fixed to `1`, encoded over 4 bits (bits 59–62); +/// - `R`: Cell resolution, ranging from `0` to `26`, encoded in bits 52–56; +/// - Remaining bits (0–51) encode the cell’s XY position in Morton order (Z-order curve). +/// +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub struct Cell(NonZeroU64); + +impl Cell { + /// Returns the inner u64 value of the cell. + pub fn get(&self) -> u64 { + self.0.get() + } + + /// 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. + /// + /// # Example + /// ``` + /// let qb_cell = qbin::Cell::new(5234261499580514303); + /// assert_eq!(qb_cell.is_valid(), true) + /// ``` + pub fn is_valid(&self) -> bool { + is_valid_cell(self.get()) + } + + /// Returns the resolution of the cell index. + /// + /// # Example + /// ``` + /// let qb_cell = qbin::Cell::new(5234261499580514303); + /// let res = qb_cell.resolution(); + /// assert_eq!(res, 10) + /// ``` + pub fn resolution(&self) -> u8 { + ((self.0.get() >> 52) & 0x1F) as u8 + } + + /// Compute the parent cell for a specific resolution. + /// + /// # Example + /// ``` + /// let qb_cell = qbin::Cell::new(5209574053332910079); + /// let parent = qb_cell.parent(2_u8); + /// assert_eq!(parent, qbin::Cell::new(5200813144682790911)) + /// ``` + pub fn parent(&self, parent_res: u8) -> Cell { + cell_to_parent(self, parent_res) + } + + // TODO: + // Add child and/or children + + /// Find the Cell's neighbor in a specific [Direction]. + /// + /// In the original JavaScript implementation, this operation is called + /// sibling. However, following the H3 naming convention, we decided + /// to name sibling's as neighbors. + /// + /// See [Direction] for allowed arguments. + /// + /// # Example + /// ``` + /// use qbin::{Cell, Direction}; + /// + /// let sibling = Cell::new(5209574053332910079).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) + } + + /// 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) + } + + /// List all Cell's neighbors. + pub fn neighbors(&self) -> [Option; 4] { + let mut neighbors = [None; 4]; + + for (i, neighbor) in neighbors.iter_mut().enumerate() { + *neighbor = self.neighbor(Direction::new_unchecked(i as u8)); + } + + neighbors + } + + // TODO: + // Add `direction_to_neighbor` -- return Direction to neighbor + + /// Computes the area of this Quadbin cell, in m². + /// + /// See also [Cell::area_km2]. + /// + /// # Example + /// ``` + /// use approx::assert_relative_eq; + /// + /// let area = qbin::Cell::new(5234261499580514303_u64).area_m2(); + /// assert_relative_eq!(area, 888546364.7859862, epsilon = 1e-6) + /// + /// ``` + pub fn area_m2(&self) -> f64 { + self.to_tile().area() + } + + /// Computes the area of this Quadbin cell, in km². + /// + /// See also [Cell::area_m2]. + /// + /// # Example + /// ``` + /// use approx::assert_relative_eq; + /// + /// let area = qbin::Cell::new(5234261499580514303_u64).area_km2(); + /// assert_relative_eq!(area, 888.5463647859862, epsilon = 1e-6) + /// + /// ``` + pub fn area_km2(&self) -> f64 { + self.area_m2() / 1_000_000_f64 + } + + /// Convert a Quadbin cell into geographic point. + /// + /// Returns a tuple with latitude and longitude in degrees. + /// + /// # Example + /// ``` + /// use qbin::Cell; + /// + /// let coords = Cell::new(5209574053332910079_u64).to_point(); + /// assert_eq!(coords, [-11.178401873711776, 33.75]); + /// ``` + /// + pub fn to_point(&self) -> [f64; 2] { + cell_to_point(self) + } + + /// Convert a Quadbin cell into a bounding box. + /// + /// Returns an array with [xmin, ymin, xmax, ymax] + /// in degrees. + /// + /// # Example + /// ``` + /// let bbox = qbin::Cell::new(5209574053332910079).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 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); + + [xmin, ymin, xmax, ymax] + } + + /// Convert a geographic point into a Quadbin cell. + /// + /// # Example + /// + /// ``` + /// let cell = qbin::Cell::from_point(-41.28303675124842, 174.77727344223067, 26); + /// assert_eq!(cell.get(), 5309133744805926483_u64) + /// ``` + pub fn from_point(lat: f64, lng: f64, res: u8) -> Cell { + point_to_cell(lat, lng, res) + } + + /// Convert a Quadbin cell into a tile. + pub(crate) fn to_tile(self) -> Tile { + cell_to_tile(&self) + } +} + +impl fmt::Display for Cell { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.get()) + } +} + +// TODO: +// 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 { @@ -18,8 +240,6 @@ pub(crate) fn is_valid_cell(cell64: u64) -> bool { (cell64 & header == header) && mode == 1 && resolution <= 26 && (cell64 & unused == unused) } -// Internal functions ------------------------------------------------ - /// Convert a tile into a Quadbin cell. pub(crate) fn tile_to_cell(tile: Tile) -> Cell { let mut x = tile.x as u64; diff --git a/src/direction.rs b/src/directions.rs similarity index 97% rename from src/direction.rs rename to src/directions.rs index d0362ff..023f12c 100644 --- a/src/direction.rs +++ b/src/directions.rs @@ -1,4 +1,4 @@ -use crate::error; +use crate::errors; use core::fmt; /// Maximum value for a direction. @@ -62,7 +62,7 @@ impl Direction { } impl TryFrom for Direction { - type Error = error::InvalidDirection; + type Error = errors::InvalidDirection; fn try_from(value: u8) -> Result { match value { diff --git a/src/error.rs b/src/errors.rs similarity index 100% rename from src/error.rs rename to src/errors.rs diff --git a/src/lib.rs b/src/lib.rs index 0fe5dd4..0af4a97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,15 +1,20 @@ #![doc = include_str!("../README.md")] +// Quadbin cell itself pub mod cells; -mod constants; -pub mod error; -pub mod utils; +pub use crate::cells::Cell; + +// Direction struct +mod directions; +pub use crate::directions::Direction; -mod types; -pub use crate::types::Cell; +// Errors +pub mod errors; -mod direction; -pub use crate::direction::Direction; +// Internal stuff +mod constants; +mod tiles; +mod utils; #[cfg(test)] mod test; diff --git a/src/test/cells.rs b/src/test/cells.rs index cab56db..9bcb921 100644 --- a/src/test/cells.rs +++ b/src/test/cells.rs @@ -1,5 +1,6 @@ -use crate::direction::Direction; -use crate::types::*; +use crate::cells::*; +use crate::directions::Direction; +use crate::tiles::*; use approx::assert_relative_eq; // Constants to save some typing diff --git a/src/test/direction.rs b/src/test/direction.rs index 5dd6a31..9c4d4a9 100644 --- a/src/test/direction.rs +++ b/src/test/direction.rs @@ -1,5 +1,5 @@ -use crate::direction::*; -use crate::error::*; +use crate::directions::*; +use crate::errors::*; #[test] fn test_valid_direction() { diff --git a/src/test/hashing.rs b/src/test/hashing.rs index 45f7315..7db0050 100644 --- a/src/test/hashing.rs +++ b/src/test/hashing.rs @@ -1,4 +1,4 @@ -use crate::types::Tile; +use crate::tiles::Tile; use crate::utils::point_cover; #[test] diff --git a/src/test/utils.rs b/src/test/utils.rs index 98519e9..ab8360a 100644 --- a/src/test/utils.rs +++ b/src/test/utils.rs @@ -1,5 +1,5 @@ -use crate::direction::Direction; -use crate::types::Tile; +use crate::directions::Direction; +use crate::tiles::Tile; use crate::utils::{point_to_tile_fraction, tile_scalefactor}; use approx::assert_relative_eq; diff --git a/src/tiles.rs b/src/tiles.rs new file mode 100644 index 0000000..2974109 --- /dev/null +++ b/src/tiles.rs @@ -0,0 +1,68 @@ +use crate::Direction; +use crate::cells::*; +use crate::utils::*; + +/// A single tile coordinates +/// +/// _Internal struct_ +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub(crate) struct Tile { + pub x: u32, + pub y: u32, + pub z: u8, +} + +impl Tile { + /// Create a new tile. + pub fn new(x: u32, y: u32, z: u8) -> Tile { + Tile { x, y, z } + } + + /// Convert to Quadbin cell. + pub fn to_cell(self) -> Cell { + tile_to_cell(self) + } + + /// Compute the tile for a longitude and latitude in a specific resolution. + pub fn from_point(lat: f64, lng: f64, res: u8) -> Self { + point_to_tile(lat, lng, res) + } + + /// Approximate tile area in square meters. + pub fn area(&self) -> f64 { + tile_area(self) + } + + /// Return tile's latitude. + /// + /// See also [Tile::to_longitude]. + /// + pub fn to_latitude(self, offset: f64) -> f64 { + tile_to_latitude(&self, offset) + } + + /// Return tile's longitude. + /// + /// See also [Tile::to_latitude]. + /// + pub fn to_longitude(self, offset: f64) -> f64 { + tile_to_longitude(&self, offset) + } + + /// Get tile's siblings. + pub fn neighbor(&self, direction: Direction) -> Option { + tile_neighbor(self, direction) + } + + /// Compute a hash from the tile. + #[allow(dead_code)] + pub fn to_hash(self) -> u64 { + to_tile_hash(&self) + } + + /// Compute a tile from the hash. + #[allow(dead_code)] + pub fn from_hash(tile_hash: u64) -> Tile { + from_tile_hash(tile_hash) + } +} diff --git a/src/types.rs b/src/types.rs deleted file mode 100644 index 2236a9d..0000000 --- a/src/types.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::Direction; -use crate::cells::*; -use crate::utils::*; -use core::{fmt, num::NonZeroU64}; - -/// Represents a cell in the Quadbin grid system at a -/// particular resolution. -/// -/// The index is encoded on 64-bit with the following bit layout: -/// -/// ```text -/// ┏━┳━━━┳━━━━┳━━━━━━━┳━━━━━━━━━━━┈┈┈┈┈┈┈┈━━━━━━━━┓ -/// ┃U┃ H ┃ M ┃ R ┃ XY in Morton order ┃ -/// ┗━┻━━━┻━━━━┻━━━━━━━┻━━━━━━━━━━━┈┈┈┈┈┈┈┈━━━━━━━━┛ -/// 63 62 59 56 52 0 -/// ``` -/// -/// Where: -/// - `U`: Unused reserved bit (bit 63), always set to `0`; -/// - `H`: Header bit (bit 62), always set to `1`; -/// - `M`: Index mode, fixed to `1`, encoded over 4 bits (bits 59–62); -/// - `R`: Cell resolution, ranging from `0` to `26`, encoded in bits 52–56; -/// - Remaining bits (0–51) encode the cell’s XY position in Morton order (Z-order curve). -/// -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub struct Cell(NonZeroU64); - -impl Cell { - /// Returns the inner u64 value of the cell. - pub fn get(&self) -> u64 { - self.0.get() - } - - /// 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. - /// - /// # Example - /// ``` - /// let qb_cell = qbin::Cell::new(5234261499580514303); - /// assert_eq!(qb_cell.is_valid(), true) - /// ``` - pub fn is_valid(&self) -> bool { - is_valid_cell(self.get()) - } - - /// Returns the resolution of the cell index. - /// - /// # Example - /// ``` - /// let qb_cell = qbin::Cell::new(5234261499580514303); - /// let res = qb_cell.resolution(); - /// assert_eq!(res, 10) - /// ``` - pub fn resolution(&self) -> u8 { - ((self.0.get() >> 52) & 0x1F) as u8 - } - - /// Compute the parent cell for a specific resolution. - /// - /// # Example - /// ``` - /// let qb_cell = qbin::Cell::new(5209574053332910079); - /// let parent = qb_cell.parent(2_u8); - /// assert_eq!(parent, qbin::Cell::new(5200813144682790911)) - /// ``` - pub fn parent(&self, parent_res: u8) -> Cell { - cell_to_parent(self, parent_res) - } - - // TODO: - // Add child and/or children - - /// Find the Cell's neighbor in a specific [Direction]. - /// - /// In the original JavaScript implementation, this operation is called - /// sibling. However, following the H3 naming convention, we decided - /// to name sibling's as neighbors. - /// - /// See [Direction] for allowed arguments. - /// - /// # Example - /// ``` - /// use qbin::{Cell, Direction}; - /// - /// let sibling = Cell::new(5209574053332910079).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) - } - - /// 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) - } - - /// List all Cell's neighbors. - pub fn neighbors(&self) -> [Option; 4] { - let mut neighbors = [None; 4]; - - for (i, neighbor) in neighbors.iter_mut().enumerate() { - *neighbor = self.neighbor(Direction::new_unchecked(i as u8)); - } - - neighbors - } - - // TODO: - // Add `direction_to_neighbor` -- return Direction to neighbor - - /// Computes the area of this Quadbin cell, in m². - /// - /// See also [Cell::area_km2]. - /// - /// # Example - /// ``` - /// use approx::assert_relative_eq; - /// - /// let area = qbin::Cell::new(5234261499580514303_u64).area_m2(); - /// assert_relative_eq!(area, 888546364.7859862, epsilon = 1e-6) - /// - /// ``` - pub fn area_m2(&self) -> f64 { - self.to_tile().area() - } - - /// Computes the area of this Quadbin cell, in km². - /// - /// See also [Cell::area_m2]. - /// - /// # Example - /// ``` - /// use approx::assert_relative_eq; - /// - /// let area = qbin::Cell::new(5234261499580514303_u64).area_km2(); - /// assert_relative_eq!(area, 888.5463647859862, epsilon = 1e-6) - /// - /// ``` - pub fn area_km2(&self) -> f64 { - self.area_m2() / 1_000_000_f64 - } - - /// Convert a Quadbin cell into geographic point. - /// - /// Returns a tuple with latitude and longitude in degrees. - /// - /// # Example - /// ``` - /// use qbin::Cell; - /// - /// let coords = Cell::new(5209574053332910079_u64).to_point(); - /// assert_eq!(coords, [-11.178401873711776, 33.75]); - /// ``` - /// - pub fn to_point(&self) -> [f64; 2] { - cell_to_point(self) - } - - /// Convert a Quadbin cell into a bounding box. - /// - /// Returns an array with [xmin, ymin, xmax, ymax] - /// in degrees. - /// - /// # Example - /// ``` - /// let bbox = qbin::Cell::new(5209574053332910079).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 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); - - [xmin, ymin, xmax, ymax] - } - - /// Convert a geographic point into a Quadbin cell. - /// - /// # Example - /// - /// ``` - /// let cell = qbin::Cell::from_point(-41.28303675124842, 174.77727344223067, 26); - /// assert_eq!(cell.get(), 5309133744805926483_u64) - /// ``` - pub fn from_point(lat: f64, lng: f64, res: u8) -> Cell { - point_to_cell(lat, lng, res) - } - - /// Convert a Quadbin cell into a tile. - pub(crate) fn to_tile(self) -> Tile { - cell_to_tile(&self) - } -} - -impl fmt::Display for Cell { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.get()) - } -} - -// TODO: -// Detect direction from neighbor https://github.com/HydroniumLabs/h3o/blob/ad2bebf52eab218d66b0bf213b14a2802bf616f7/src/base_cell.rs#L135C1-L150C6 - -// -------------------------------------------------------- - -/// A single tile coordinates -/// -/// _Internal struct_ -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub(crate) struct Tile { - pub x: u32, - pub y: u32, - pub z: u8, -} - -impl Tile { - /// Create a new tile. - pub fn new(x: u32, y: u32, z: u8) -> Tile { - Tile { x, y, z } - } - - /// Convert to Quadbin cell. - pub fn to_cell(self) -> Cell { - tile_to_cell(self) - } - - /// Compute the tile for a longitude and latitude in a specific resolution. - pub fn from_point(lat: f64, lng: f64, res: u8) -> Self { - point_to_tile(lat, lng, res) - } - - /// Approximate tile area in square meters. - pub fn area(&self) -> f64 { - tile_area(self) - } - - /// Return tile's latitude. - /// - /// See also [Tile::to_longitude]. - /// - pub fn to_latitude(self, offset: f64) -> f64 { - tile_to_latitude(&self, offset) - } - - /// Return tile's longitude. - /// - /// See also [Tile::to_latitude]. - /// - pub fn to_longitude(self, offset: f64) -> f64 { - tile_to_longitude(&self, offset) - } - - /// Get tile's siblings. - pub fn neighbor(&self, direction: Direction) -> Option { - tile_neighbor(self, direction) - } - - /// Compute a hash from the tile. - #[allow(dead_code)] - pub fn to_hash(self) -> u64 { - to_tile_hash(&self) - } - - /// Compute a tile from the hash. - #[allow(dead_code)] - pub fn from_hash(tile_hash: u64) -> Tile { - from_tile_hash(tile_hash) - } -} diff --git a/src/utils.rs b/src/utils.rs index 33a2650..31d4cc1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,6 @@ use crate::constants::*; -use crate::direction::Direction; -use crate::types::Tile; +use crate::directions::Direction; +use crate::tiles::Tile; use std::f64::consts::PI; /// Clip a value between a minimum and maximum value From cb92d5461480cc2585f0ade747893be6e71db3c1 Mon Sep 17 00:00:00 2001 From: Anatolii Tsyplenkov Date: Tue, 29 Apr 2025 11:31:27 +1200 Subject: [PATCH 07/10] refactor: change version to 0.1.1 --- Cargo.lock | 2 +- Cargo.toml | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7c19a7..1a99b4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -408,7 +408,7 @@ dependencies = [ [[package]] name = "qbin" -version = "0.2.0" +version = "0.1.1" dependencies = [ "approx", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 19fef98..4676fdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qbin" -version = "0.2.0" +version = "0.1.1" authors = ["Anatoly Tsyplenkov "] edition = "2024" license = "MIT" @@ -13,7 +13,6 @@ keywords = ["quadbin", "quadkey", "spatial", "spatial-index"] categories = ["data-structures", "science::geo"] [dependencies] -geohash = "0.13.1" [dev-dependencies] approx = "0.5.1" From 476e7c7274f9459d6a13017b8cd737a23401bf55 Mon Sep 17 00:00:00 2001 From: Anatolii Tsyplenkov Date: Tue, 29 Apr 2025 12:00:49 +1200 Subject: [PATCH 08/10] ci: update CI workflow --- .github/workflows/rust-ci.yml | 67 +++++++++++++++++++++++++++++++++++ .github/workflows/rust.yml | 29 --------------- 2 files changed, 67 insertions(+), 29 deletions(-) create mode 100644 .github/workflows/rust-ci.yml delete mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml new file mode 100644 index 0000000..5899e9f --- /dev/null +++ b/.github/workflows/rust-ci.yml @@ -0,0 +1,67 @@ +name: Rust CI + +on: + push: + branches: [ "main", "master" ] + pull_request: + branches: [ "main", "master" ] + +env: + CARGO_TERM_COLOR: always + PKG_CONFIG_PATH: /usr/lib/pkgconfig + +jobs: + build: + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + arch: [x64, arm64] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose -- --test-threads=2 + + clippy: + name: Lints + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: Check the lints + run: cargo clippy --tests --verbose -- -D warnings + + + rustfmt: + name: Formatting + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Check the formatting + run: cargo fmt -- --check --verbose + + lychee: + name: Links + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v4 + - name: Check the links + uses: lycheeverse/lychee-action@v2 + with: + args: -v *.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 220cbd9..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Rust CI - -on: - push: - branches: [ "main", "master" ] - pull_request: - branches: [ "main", "master" ] - -env: - CARGO_TERM_COLOR: always - PKG_CONFIG_PATH: /usr/lib/pkgconfig - -jobs: - build: - strategy: - matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] - arch: [x64, arm64] - - runs-on: ${{ matrix.os }} - - steps: - - uses: actions/checkout@v4 - - name: Build - run: cargo build --verbose - - name: Run tests - run: cargo test --verbose -- --test-threads=2 - - name: Run linter - run: cargo clippy --verbose \ No newline at end of file From e62de13c7b7470e97dc276af7c025219ed956631 Mon Sep 17 00:00:00 2001 From: Anatolii Tsyplenkov Date: Tue, 29 Apr 2025 12:01:13 +1200 Subject: [PATCH 09/10] docs: introduce git-cliff for more meaningful changelog --- cliff.toml | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 cliff.toml diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..74e75df --- /dev/null +++ b/cliff.toml @@ -0,0 +1,84 @@ +# git-cliff ~ default configuration file +# https://git-cliff.org/docs/configuration +# +# Lines starting with "#" are comments. +# Configuration options are organized into tables and keys. +# See documentation for more information on available options. + +[changelog] +# template for the changelog header +header = """ +# Changelog\n +All notable changes to this project will be documented in this file.\n +""" +# template for the changelog body +# https://keats.github.io/tera/docs/#introduction +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}*({{ commit.scope }})* {% endif %}\ + {% if commit.breaking %}[**breaking**] {% endif %}\ + {{ commit.message | upper_first }}\ + {% endfor %} +{% endfor %}\n +""" +# template for the changelog footer +footer = """ + +""" +# remove the leading and trailing s +trim = true +# postprocessors +postprocessors = [ + # { pattern = '', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL +] +# render body even when there are no releases to process +# render_always = true +# output file path +# output = "test.md" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Replace issue numbers + #{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](/issues/${2}))"}, + # Check spelling of the commit with https://github.com/crate-ci/typos + # If the spelling is incorrect, it will be automatically fixed. + #{ pattern = '.*', replace_command = 'typos --write-changes -' }, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "🚀 Features" }, + { message = "^fix", group = "🐛 Bug Fixes" }, + { message = "^doc", group = "📚 Documentation", skip = true}, + { message = "^perf", group = "⚡ Performance" }, + { message = "^refactor", group = "🚜 Refactor", skip = true}, + { 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}, + { body = ".*security", group = "🛡️ Security" }, + { message = "^revert", group = "◀️ Revert" }, + { message = ".*", group = "💼 Other" }, +] +# filter out the commits that are not matched by commit parsers +filter_commits = true +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" From 3042b065782ad25551fe5d086db0cea4343d7fa3 Mon Sep 17 00:00:00 2001 From: Anatolii Tsyplenkov Date: Tue, 29 Apr 2025 12:01:29 +1200 Subject: [PATCH 10/10] lint: small refactoring and lints removal --- src/test/cells.rs | 6 +++--- src/test/{direction.rs => directions.rs} | 2 +- src/test/mod.rs | 4 ++-- src/test/{utils.rs => tiles.rs} | 0 4 files changed, 6 insertions(+), 6 deletions(-) rename src/test/{direction.rs => directions.rs} (93%) rename src/test/{utils.rs => tiles.rs} (100%) diff --git a/src/test/cells.rs b/src/test/cells.rs index 9bcb921..9b09b14 100644 --- a/src/test/cells.rs +++ b/src/test/cells.rs @@ -99,8 +99,8 @@ fn test_cell_to_bbox() { for i in cases.iter() { let bbox = Cell::new(*i).to_bbox(); - assert_eq!(bbox[0] < bbox[2], true); - assert_eq!(bbox[1] < bbox[3], true); + assert!(bbox[0] < bbox[2]); + assert!(bbox[1] < bbox[3]); } } @@ -147,7 +147,7 @@ fn test_cell_to_parent_invalid_resolution() { #[test] fn test_cell_area() { let area = Cell::new(5209574053332910079_u64).area_m2(); - assert_relative_eq!(area, 6023040823252.6641, epsilon = 1e-2); + assert_relative_eq!(area, 6023040823252.664, epsilon = 1e-2); } // Find cell's neighbors diff --git a/src/test/direction.rs b/src/test/directions.rs similarity index 93% rename from src/test/direction.rs rename to src/test/directions.rs index 9c4d4a9..a044a50 100644 --- a/src/test/direction.rs +++ b/src/test/directions.rs @@ -6,7 +6,7 @@ fn test_valid_direction() { let dirs = [0, 1, 2, 3]; for val in dirs.iter() { - assert_eq!(u8::from(Direction::new_unchecked(*val)), *val as u8); + assert_eq!(u8::from(Direction::new_unchecked(*val)), { *val }); assert_eq!(u64::from(Direction::new_unchecked(*val)), *val as u64); assert_eq!(usize::from(Direction::new_unchecked(*val)), *val as usize); } diff --git a/src/test/mod.rs b/src/test/mod.rs index 002bf6c..191cc0d 100644 --- a/src/test/mod.rs +++ b/src/test/mod.rs @@ -1,4 +1,4 @@ mod cells; -mod direction; +mod directions; mod hashing; -mod utils; +mod tiles; diff --git a/src/test/utils.rs b/src/test/tiles.rs similarity index 100% rename from src/test/utils.rs rename to src/test/tiles.rs