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
-[](https://github.com/atsyplenkov/qbin/blob/main/LICENSE)
-[](https://crates.io/crates/qbin)
-[](https://docs.rs/qbin)
-[](https://github.com/atsyplenkov/qbin/actions/workflows/rust.yml)
-[](https://codecov.io/gh/atsyplenkov/qbin)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
| |